# Swiggy Builders Club - Full Documentation > Swiggy Builders Club is the ecosystem program around Swiggy's MCP stack - Food, Instamart, Dineout, and Scenes. ## Docs ### Multi-turn cart state Carrying cart identity across user turns on a stateless protocol. Swiggy's cart state lives server-side, keyed to the authenticated session. Tools like `update_food_cart` and `update_cart` mutate that server-side cart; subsequent `get_food_cart` / `get_cart` calls see the mutation. That means: your agent doesn't need to carry cart IDs or contents between turns. Just call `get_*_cart` at the top of any turn that might touch the cart, and you'll see the truth. ## Pattern: refresh at turn boundary ```ts // Every turn that might involve cart state, start with: const cart = await client.callTool({ name: "get_food_cart" }); // Decide next step based on what's actually in the cart server-side, // NOT based on what you remember from the last turn ``` This avoids drift between "what the agent thinks is in the cart" and "what Swiggy actually has". ## Pattern: confirm before mutating Because the cart is shared state, mutating it in a multi-turn conversation requires care: ``` Turn 1: User: "Add chicken biryani" Agent: [update_food_cart(add biryani)] "Added 1 chicken biryani (₹349). Anything else?" Turn 2: User: "Make it 2" Agent: [get_food_cart → sees 1 biryani] [update_food_cart(set biryani quantity to 2)] "Now 2 biryanis (₹698). Anything else?" Turn 3: User: "Actually, place the order" Agent: [get_food_cart → confirms current state] "Order: 2 chicken biryanis, total ₹698, COD. Place now?" ``` Call `get_food_cart` **before** `place_food_order` regardless of how confident you are - the user may have edited in the Swiggy app between turns. ## Restaurant switch (Food) A Food cart binds to one restaurant. If the user asks for something from a different restaurant, the cart flushes automatically. Surface this: ``` User: "Add butter chicken from Punjab Grill" Agent: [currently 2 biryanis from Biryani House in cart] "That will clear your Biryani House cart (2 chicken biryanis, ₹698). Continue?" ``` If you don't warn, the user loses work silently - bad for trust. ## Address switch (Instamart) An Instamart cart binds to the delivery address (different addresses may have different serviceability and stock). Changing address mid-cart risks serviceability and stock failures on the new address. Safer pattern: `clear_cart` before switching address. ## Across server boundaries If your agent uses Food + Instamart + Dineout + Scenes in one session: - **Carts are per-server, not shared.** A Food cart doesn't affect an Instamart cart. - **Authentication is shared.** One OAuth token works across all four servers. - **Orders are per-server.** `get_food_orders` won't show Instamart orders. ## Abandoned carts Carts have a TTL. If the user walks away mid-conversation and returns later, a stale cart may return `CART_EXPIRED`. Re-fetch, rebuild if necessary, confirm with the user before re-placing items. ## Don't cache cart state in your agent's memory Tempting optimization: "I'll just remember what I added so I don't have to re-fetch". Don't. The authoritative copy is server-side, and: - The user may edit in the Swiggy app. - Items may go out of stock between turns. - Prices may change. - Coupons may become invalid. `get_*_cart` is cheap (milliseconds). Always read before you mutate or confirm. --- ### Voice vs chat The same Swiggy tool, different response contracts. Design for TTS and rich cards separately. A `search_restaurants` response that works great in Claude's chat UI (long list, rich cards, ratings, distances) is a disaster on a car's voice assistant - it'll read 18 restaurant names while the user tries to change lanes. Voice and chat surfaces want different things from the same tool. Your agent's job is to shape the answer for its surface. ## When to assume voice You're on voice if any of these are true: - The client is a car, TV, smart speaker, or ambient surface. - The response will be TTS-rendered without a screen. - The user typed no input (they spoke). - Your framework's surface metadata says voice (some frameworks expose `ctx.surface === "voice"`). Otherwise, assume chat: a visible screen, the user can scan, widgets help. ## Voice response contract Your system prompt should include something like: > **Note** > > You are on a voice surface. Your responses will be spoken, not shown. Follow these rules: > > - **Maximum 3 items** in any list you read aloud. Summarise "+ 5 more" if there are more. > - Say **prices in Indian rupees** aloud - "₹249" → "two hundred and forty-nine rupees". > - Spell out **delivery times** naturally - "35-40 MINS" → "about 40 minutes". > - **Confirm before acting**: always say back the cart total and delivery time before calling `place_food_order`. Wait for "yes". > - **Never read tool IDs aloud** - `addressId`, `restaurantId`, `spinId` are never voice-appropriate. > - Default to the user's **saved Home address** without asking, unless they said otherwise. ## Chat response contract ``` You are on a chat surface (Claude, ChatGPT, or similar). Your responses will be rendered with markdown and may include widgets. - Present up to 8 restaurants in a search result as a concise list with name, cuisine, rating, distance, delivery time. - Use the widget returned alongside search_restaurants (restaurant-card) when your client supports rendering it. - Show cart items as a markdown table with item, quantity, price, total. - Still confirm before place_food_order, but in text ("Confirm order? Reply yes to place."). ``` ## Sample voice exchange ``` User: Order biryani Agent: I found two options near your home address. The closest is Biryani House, about 30 minutes, forty-nine rupees off with a welcome coupon. Want that one? User: Yes Agent: One chicken biryani from Biryani House. Total four hundred and sixty nine rupees, cash on delivery. Confirm to place? User: Confirm Agent: Placed. You'll see delivery updates in the Swiggy app. ``` ## Sample chat exchange ```markdown **User**: Order biryani **Agent**: I found these near your home: | Restaurant | Rating | Distance | Delivery | |------------------|--------|----------|----------| | Biryani House | 4.5 ★ | 2.1 km | 30 min | | Paradise Biryani | 4.3 ★ | 3.8 km | 40 min | | Kebab Factory | 4.4 ★ | 5.2 km | 45 min | [Restaurant card widget for Biryani House] Which one? ``` ## Tool-selection differences Some tools are more useful on voice than chat: | Tool | Voice | Chat | | --- | --- | --- | | `your_go_to_items` (Instamart reorder) | **Perfect** - "reorder your usual?" one-shot | Also good, but search is fine on screen | | `search_menu` with many results | Compress to top 3 | Show up to 10 | | `fetch_food_coupons` | Read top 1 | Show whole list | | `track_food_order` | Say ETA only | Show full timeline | ## What Swiggy does for you Tool responses include fields optimized for both surfaces: - `shortDescription` (voice-friendly, 1 sentence) - `longDescription` (chat-friendly, includes structured data) - `deliveryTimeSpoken` (e.g. "about 30 minutes") vs `deliveryTimeRange` (e.g. "25-35 MIN") Use the right field for your surface. ## Guardrails common to both - **Never autonomously place an order** without user confirmation. Surfaces differ in the *shape* of the confirmation, not its necessity. - **Always surface distance** for far restaurants (>5 km on Food, >10 km on Dineout). - **Respect the ₹1000 cart cap** on Food; tell the user before they pick an 8th item they can't afford. - **Never read raw IDs, tokens, or internal codes** aloud or in screen UI. --- ### Build Recipes and patterns for shipping agents that use Swiggy MCP. You're past the quickstart - your agent can call `get_addresses`. Now build the thing. This tab is the practical half of the docs: end-to-end journeys, agent patterns, widgets, and go-live. ## Recipes End-to-end journeys you can paste into an agent today. - [Order food end-to-end](https://mcp.swiggy.com/builders/docs/build/recipes/order-food.md) - 7-tool Food journey, COD payment, tracking. - [Order groceries end-to-end](https://mcp.swiggy.com/builders/docs/build/recipes/order-groceries.md) - Instamart discover → cart → checkout → track. - [Pay with UPI end-to-end](https://mcp.swiggy.com/builders/docs/build/recipes/pay-with-upi.md) - the shared Payment stage: picker → paid order → confirm, on any server. - [Book a table](https://mcp.swiggy.com/builders/docs/build/recipes/book-a-table.md) - Dineout availability + reservation. - [Plan my evening (combined)](https://mcp.swiggy.com/builders/docs/build/recipes/combined.md) - Food + Dineout in one agent turn. ## Agent patterns How to shape Swiggy MCP into agents that don't embarrass themselves in production. - [Voice vs chat](https://mcp.swiggy.com/builders/docs/build/agent-patterns/voice-vs-chat.md) - different response contracts for TTS and rich-card surfaces. - [Multi-turn cart state](https://mcp.swiggy.com/builders/docs/build/agent-patterns/multi-turn-state.md) - carrying cart identity across turns on a stateless protocol. ## Surfaces - [Widgets](https://mcp.swiggy.com/builders/docs/build/widgets.md) - MCP-UI fragments (restaurant cards, menu items, cart widgets) that agents can hand back to chat clients for rendering. ## Ship - [Ship to production](https://mcp.swiggy.com/builders/docs/build/ship-to-production.md) - retries, observability, the go-live checklist. --- ### Book a table Dineout journey - find a restaurant, check availability, reserve. Dineout is Swiggy's table-reservation surface across 50+ Indian cities. The flow is compact: find → check slots → book → confirm. ## The flow ``` get_saved_locations │ ▼ search_restaurants_dineout ──► get_restaurant_details │ ▼ get_available_slots ──► book_table ──► get_booking_status ``` ## Step 1 - Start with a location ```ts const locations = await client.callTool({ name: "get_saved_locations" }); // Unlike Food/Instamart, Dineout returns lat/lng explicitly for "near me" queries ``` If the user asks "restaurants near me for Friday dinner", use the first saved location's coordinates. ## Step 2 - Search ```ts const restaurants = await client.callTool({ name: "search_restaurants_dineout", arguments: { lat: locations.data[0].lat, lng: locations.data[0].lng, query: "italian", }, }); ``` Results include availability status, offers, cuisines, and price range. Filter to restaurants where availability is `"AVAILABLE"` before presenting. ## Step 3 - Dig into details ```ts const details = await client.callTool({ name: "get_restaurant_details", arguments: { restaurantId: restaurants.data.restaurants[0].id }, }); // details.data: ratings, amenities, menu images, exclusive Dineout deals ``` ## Step 4 - Check available slots ```ts const slots = await client.callTool({ name: "get_available_slots", arguments: { restaurantId: details.data.id, date: "2026-05-01", guestCount: 4, }, }); // slots.data has 7-day forward availability, broken into breakfast/lunch/dinner bands ``` Surface slot times in the user's timezone (all restaurants are in India; IST applies). ## Step 5 - Book ```ts const booking = await client.callTool({ name: "book_table", arguments: { restaurantId: details.data.id, slotId: slots.data.slots[0].slotId, guestCount: 4, }, }); ``` **Important**: `book_table` is **not idempotent**. On 5xx, call [`get_booking_status`](https://mcp.swiggy.com/builders/docs/reference/dineout/get_booking_status.md) with the restaurant and slot before retrying. ## Step 6 - Confirm ```ts const status = await client.callTool({ name: "get_booking_status", arguments: { bookingId: booking.data.bookingId }, }); ``` Send the user the confirmation number and the restaurant's address. ## Agent prompt > **Note** > > You help users book restaurant tables on Swiggy Dineout. Resolve the user's location first (via `get_saved_locations` or lat/lng), then search. Always confirm slot date, time, and party size with the user before `book_table`. Show restaurant details (amenities, deals) before asking for slot confirmation. ## Common errors - `SLOT_UNAVAILABLE` → slot filled; refetch availability and offer alternatives. - `RESTAURANT_NOT_BOOKABLE` → restaurant isn't Dineout-enabled; offer dine-in walk-in guidance or a Food order instead. - `BOOKING_WINDOW_CLOSED` → outside booking hours; present next available day. Full catalogue: [errors](https://mcp.swiggy.com/builders/docs/reference/errors.md). --- ### Plan my evening (combined) One user ask, two MCP servers - Food delivery and Dineout reservations composed in a single agent turn. A fun showcase of MCP's tool-composition strength: the user says "plan my evening for 4 - dinner out, dessert delivered later", and your agent fans out across two Swiggy servers. ## The ask > **Note** > > "Plan my evening for Friday. I want dinner out with 3 friends around 8pm, something Italian in Indiranagar. Then order dessert to my place for 10pm." This requires both [Dineout](https://mcp.swiggy.com/builders/docs/reference/dineout/index.md) (for the reservation) and [Food](https://mcp.swiggy.com/builders/docs/reference/food/index.md) (for the delivery). Both servers share the underlying Swiggy session - one OAuth, two MCP URLs. ## Connect both servers Most frameworks allow multiple MCP servers side-by-side. Example with OpenAI Agents SDK: ```ts const dineout = new MCPServerStreamableHttp({ url: "https://mcp.swiggy.com/dineout", requestInit: { headers: { Authorization: `Bearer ${token}` } }, }); const food = new MCPServerStreamableHttp({ url: "https://mcp.swiggy.com/food", requestInit: { headers: { Authorization: `Bearer ${token}` } }, }); const agent = new Agent({ name: "EveningPlanner", instructions: "Plan the user's evening using both servers.", mcpServers: [dineout, food], }); ``` Tool names don't collide - `search_restaurants` (Food) and `search_restaurants_dineout` are distinct. ## What the agent does Internally, the model orchestrates the two flows in parallel: ``` ┌──────────────────────────┐ │ User: Plan my evening │ └─────────────┬─────────────┘ │ ┌─────────────────┴─────────────────┐ ▼ ▼ get_saved_locations get_addresses (Food) │ │ ▼ │ search_restaurants_dineout │ (query="italian", │ lat/lng of saved home) │ │ │ ▼ │ get_restaurant_details │ │ │ ▼ │ get_available_slots │ (date=Friday, guestCount=4) │ │ │ ▼ │ book_table (later, after dinner recommendation) │ ▼ search_restaurants (query="gelato", addressId=home) │ ▼ get_restaurant_menu │ ▼ update_food_cart │ ▼ place_food_order (scheduled 10pm) ``` ## Agent prompt for this pattern > **Note** > > You compose Swiggy Dineout and Swiggy Food tools to plan user evenings. When the user asks for a restaurant reservation AND food delivery in one request, handle them sequentially: reservation first (so they see slot options early), then dessert/delivery second. Always confirm reservation details and food cart separately before calling `book_table` and `place_food_order`. ## Handle auth expiry across both servers If one server returns 401, your session is gone for both. Re-run the OAuth flow once, update the bearer for both clients, retry. ## Gotchas - **Scheduling**: `place_food_order` places orders for immediate delivery. Swiggy Food doesn't support future-scheduled delivery in v1 - if the user wants dessert at 10pm exactly, your agent needs to remind them / place the order at the right time. - **Address vs location**: Dineout uses lat/lng for "near me"; Food uses `addressId`. They're different scopes. Don't try to pass an addressId to Dineout search. - **Cart conflicts**: Food cart is per-restaurant. If your agent adds to cart, then searches a different restaurant, the cart will be flushed. Surface that explicitly. --- ### Order food end-to-end The canonical 7-tool Food journey - from address to placed order to delivery tracking. The full food-ordering journey across Swiggy's Food MCP server. Pays by COD; to take payment with UPI instead, see [Pay with UPI](https://mcp.swiggy.com/builders/docs/build/recipes/pay-with-upi.md). ₹1000 cart cap. Pseudo-code in TypeScript; the same sequence works in any framework. ## The flow ``` get_addresses │ ▼ search_restaurants ──► get_restaurant_menu │ ▼ update_food_cart ◄── fetch_food_coupons │ │ ▼ │ get_food_cart ◄── apply_food_coupon │ ▼ place_food_order │ ▼ track_food_order ``` ## Step 1 - Resolve the delivery address ```ts const addresses = await client.callTool({ name: "get_addresses" }); const home = addresses.data.find((a) => a.label === "Home") ?? addresses.data[0]; if (!home) throw new Error("User has no saved addresses; prompt them to add one."); ``` If the user has no saved addresses, walk them through [`create_address`](https://mcp.swiggy.com/builders/docs/reference/food/create_address.md) — coordinates are optional; the server geocodes from the address. [`get_addresses`](https://mcp.swiggy.com/builders/docs/reference/food/get_addresses.md) returns label, addressId, and display text - never raw coordinates. ## Step 2 - Find restaurants ```ts const restaurants = await client.callTool({ name: "search_restaurants", arguments: { addressId: home.id, query: "biryani" }, }); ``` Check `availabilityStatus` for each result - only recommend those marked `"OPEN"`. Sort by a mix of distance and rating; always surface distance when picking far restaurants so the user isn't surprised. ## Step 3 - Browse the menu ```ts const menu = await client.callTool({ name: "get_restaurant_menu", arguments: { restaurantId: restaurants.data.restaurants[0].id }, }); ``` Menus have categories, items, variants, and add-ons. Use [`search_menu`](https://mcp.swiggy.com/builders/docs/reference/food/search_menu.md) for keyword search within (or across) restaurants. ## Step 4 - Build the cart ```ts await client.callTool({ name: "update_food_cart", arguments: { restaurantId: menu.data.restaurantId, items: [ { itemId: menu.data.items[0].id, quantity: 1 }, ], }, }); ``` Cart is tied to a single restaurant. Changing restaurant flushes the cart. Use [`flush_food_cart`](https://mcp.swiggy.com/builders/docs/reference/food/flush_food_cart.md) explicitly when the user starts over. ## Step 5 - Apply a coupon (optional) ```ts const coupons = await client.callTool({ name: "fetch_food_coupons" }); // This recipe pays by COD - filter to coupons that don't require online payment // (paying by UPI instead? see the Pay-with-UPI recipe; online-payment coupons apply there) const codCoupon = coupons.data.find((c) => !c.requiresOnlinePayment); if (codCoupon) { await client.callTool({ name: "apply_food_coupon", arguments: { code: codCoupon.code }, }); } ``` ## Step 6 - Confirm and place the order ```ts const cart = await client.callTool({ name: "get_food_cart" }); // Swiggy v1: hard ₹1000 cap on Builders Club orders if (cart.data.total > 1000) { throw new Error("Cart exceeds ₹1000 cap - ask user to reduce items."); } // Surface to the user before placing // "Your order is: . Total ₹. Place now? (yes / no)" const order = await client.callTool({ name: "place_food_order", arguments: { paymentMethod: "COD" }, }); ``` **Critical**: `place_food_order` is **not idempotent**. If it fails with 5xx, call [`get_food_orders`](https://mcp.swiggy.com/builders/docs/reference/food/get_food_orders.md) to check if the order actually placed before retrying. See [ship to production](https://mcp.swiggy.com/builders/docs/build/ship-to-production.md). ## Step 7 - Track the order ```ts const status = await client.callTool({ name: "track_food_order", arguments: { orderId: order.data.orderId }, }); // Poll no faster than every 10 seconds; delivery-partner ETA updates arrive at that cadence ``` ## Full agent prompt Good system prompt for the agent driving this flow: > **Note** > > You help users order food on Swiggy. Always resolve the user's saved address via `get_addresses` before searching. Only recommend restaurants with `availabilityStatus: "OPEN"`. Confirm the cart and total with the user before calling `place_food_order` - that call places a real order. This recipe pays by COD; filter coupons to those not requiring online payment. (To take UPI payment, follow the Pay-with-UPI recipe instead.) Never exceed ₹1000 cart total. ## What can go wrong Until the symbolic error-code registry ships (see [errors](https://mcp.swiggy.com/builders/docs/reference/errors.md)), classify by `error.message` text and HTTP status. Expect: - **Restaurant closed** between search and order → re-run `search_restaurants`. - **Coupon requires online payment** → not usable on the COD path; filter upstream, or pay with [UPI](https://mcp.swiggy.com/builders/docs/build/recipes/pay-with-upi.md). - **Minimum order not met** → prompt user to add items. - **Upstream shedding / timeout** → exponential backoff; capacity questions go to [rate-limits](https://mcp.swiggy.com/builders/docs/operate/rate-limits.md). --- ### Order groceries end-to-end Full Instamart journey - find products, build a cart, checkout, track delivery. Instamart is Swiggy's quick-commerce grocery service across 1000+ Indian cities. Same shape as Food (discover → cart → order → track), different catalogue. ## The flow ``` get_addresses │ ▼ search_products ──► update_cart ──► get_cart ──► checkout ──► track_order ▲ │ └──────────────────┘ your_go_to_items (bypass search) ``` ## Step 1 - Resolve the delivery address ```ts const addresses = await client.callTool({ name: "get_addresses" }); const home = addresses.data.find((a) => a.label === "Home") ?? addresses.data[0]; ``` If the user has no addresses, walk them through [`create_address`](https://mcp.swiggy.com/builders/docs/reference/instamart/create_address.md). ## Step 2 - Find products (or reorder) Two paths. For quick reorders: ```ts const goTo = await client.callTool({ name: "your_go_to_items", arguments: { addressId: home.id }, }); // goTo.data has frequently-ordered SKUs - present as one-tap add ``` For search: ```ts const results = await client.callTool({ name: "search_products", arguments: { addressId: home.id, query: "bananas" }, }); ``` Each product returns one or more `variations` with their own `spinId` (the SKU-level identifier). You add variations to the cart, not the parent product. The response may also include a `similarProducts` array — related items shown when the catalogue API finds relevant alternatives. Present these in a separate "Similar Items" section below the main results. ## Step 3 - Build the cart ```ts await client.callTool({ name: "update_cart", arguments: { items: [ { spinId: results.data.products[0].variants[0].spinId, quantity: 2 }, ], }, }); ``` Swapping address mid-cart? Don't. Run [`clear_cart`](https://mcp.swiggy.com/builders/docs/reference/instamart/clear_cart.md) first to avoid cross-address SKU mismatches. ## Step 4 - Review the cart ```ts const cart = await client.callTool({ name: "get_cart" }); // cart.data has items[], bill breakdown, payment methods available ``` Check `ADDRESS_NOT_SERVICEABLE` or `MIN_ORDER_NOT_MET` errors; Instamart has a ₹99 minimum and service-area restrictions. ## Step 5 - Checkout ```ts const order = await client.callTool({ name: "checkout", arguments: { paymentMethod: "COD" }, }); ``` This uses COD. To take payment with UPI, call [`get_payment_options`](https://mcp.swiggy.com/builders/docs/reference/instamart/get_payment_options.md) first and pass the picked method into `checkout` (`paymentMethod:"UPI"` + `intentApp`/`generateUPIQR`) - see the [Pay with UPI](https://mcp.swiggy.com/builders/docs/build/recipes/pay-with-upi.md) recipe. Same non-idempotency rule as Food: if `checkout` 5xxs, check [`get_orders`](https://mcp.swiggy.com/builders/docs/reference/instamart/get_orders.md) before retrying. ## Step 6 - Track ```ts const status = await client.callTool({ name: "track_order", arguments: { orderId: order.data.orderId }, }); // ETA typically 10-20 min post-checkout ``` Poll no faster than every 10s. ## Agent prompt > **Note** > > You help users shop on Swiggy Instamart. Start by resolving the user's saved address. Offer `your_go_to_items` for quick reorders; use `search_products` for new queries. Always confirm the cart and total before `checkout`. When the user is ready to pay, call `get_payment_options` — both UPI (scan-QR / app intent) and Cash are supported; Cash is the fallback when UPI isn't available. ## Common errors Until the symbolic `error.code` registry ships (see [errors](https://mcp.swiggy.com/builders/docs/reference/errors.md)), classify by `error.message` text. Expect: - **Item out of stock** at this address → suggest alternatives from `search_products`. - **Address not serviceable** → Instamart doesn't deliver here; ask for another address or offer Food. - **Minimum order not met** (cart under ₹99) → prompt user to add items. - **Cart expired / abandoned** → rebuild the cart. --- ### Pay with UPI end-to-end The shared UPI Payment stage - from picker to paid order to tracking - across Food, Instamart, Dineout, and Scenes. In-chat UPI payment. This recipe picks up once the cart (or paid booking) is ready and walks the **shared Payment stage**: `get_payment_options → place-order → check_payment_status → confirm_order`. The three shared tools (`get_payment_options`, `check_payment_status`, `confirm_order`) — plus each server's own place-order tool — work across all four servers. COD is the fallback on Food, Instamart, and Dineout: when UPI isn't available for the cart, the picker offers Cash automatically. Scenes is the exception — event tickets are prepaid, so no Cash method is ever returned there. ## The flow ``` cart ready (get_food_cart / get_cart / create_cart) │ ▼ get_payment_options ─────► renders the payment picker (UPI apps + scan-QR, or Cash) │ ▼ place_food_order / checkout / book_table (paymentMethod:"UPI" + intentApp OR generateUPIQR) │ → order created in PENDING_PAYMENT (returns paasId + orderId) ▼ check_payment_status ◄── the widget auto-polls; auto-finalizes on any terminal outcome │ ▼ confirm_order ─────────► PENDING_PAYMENT → PLACED (or failed) │ ▼ track_food_order / track_order / get_booking_status ``` ## Step 1 - Fetch payment options ```ts const opts = await client.callTool({ name: "get_payment_options", // Food only: pass the same addressId you used for get_food_cart (it's echoed to the picker) arguments: { addressId: home.id }, }); ``` One call returns **both** device surfaces merged into a picker widget: - `data.platforms.mobile.methods[]` - UPI apps (intent) - `data.platforms.desktop.methods[]` - scan-QR - `data.cod` - Cash, when available The widget auto-detects the device and promotes the matching surface - you don't ask the user what device they're on. If UPI isn't available for the cart, `platforms` is omitted and only Cash is offered via `data.allMethods`. See [`get_payment_options`](https://mcp.swiggy.com/builders/docs/reference/food/get_payment_options.md). ## Step 2 - Place the order with the picked method The user taps a method in the picker; pass their choice into the place-order tool byte-for-byte: ```ts // Mobile UPI app: const order = await client.callTool({ name: "place_food_order", arguments: { addressId: home.id, paymentMethod: "UPI", intentApp: picked.id, // the method id from get_payment_options, echoed exactly }, }); // Desktop scan-QR: paymentMethod:"UPI" + generateUPIQR:true (no intentApp) ``` This creates the order in **`PENDING_PAYMENT`** and returns `paasId` + `orderId`. Echo them into `check_payment_status`. The `confirm_order` args differ per server (see the [contract](#per-server-contract) below): **Food** uses `orderId` + `addressId` + `lat` + `lng` (**not** `paasId`); **Instamart / Dineout / Scenes** use `orderId` + `paasId`. ## Step 3 - Wait for payment (don't babysit the poll) `check_payment_status` is a **long-poll** (the server holds ~19s). The confirmation widget polls it automatically and auto-finalizes on any terminal outcome, so on a widget host **you do nothing here**. Only if you drive your own loop (no widget), cap it and read `data.status`: ```ts // self-hosted clients only - the widget does this for you. // Food: ALSO echo addressId + lat + lng, else the server can't reconcile the order and it stays pending. const deadline = Date.now() + order.data.maxTimeToPollForInMs; // honor the window place-order gave you let s; do { s = (await client.callTool({ name: "check_payment_status", arguments: { paasId: order.data.paasId, orderId: order.data.orderId, addressId: home.id, lat: home.lat, lng: home.lng }, })).data; if (s.terminal) break; await sleep(order.data.pollingIntervalInMs); // gentle — it's a ~19s long-poll } while (Date.now() < deadline); // capped — never tight-loop ``` Branch on `data.status`: - `success` / `paid` → placed (usually already confirmed for you; `data.confirmed`) - `failed` → don't confirm; re-show `get_payment_options` for a fresh attempt - `cancelled` → order was cancelled (refund if already paid); tell the user, don't retry - `cart_changed` → cart price/stock changed, so the order was NOT placed (not a payment failure); ask the user to review the cart, then place again - `refund-initiated` → a refund is already underway; tell the user, don't retry - `pending` → keep waiting, capped - never tight-loop (it stresses the payment cache) See [`check_payment_status`](https://mcp.swiggy.com/builders/docs/reference/food/check_payment_status.md). ## Step 4 - Finalize On the widget path this is automatic. If you hit your own cap while still `pending`, call [`confirm_order`](https://mcp.swiggy.com/builders/docs/reference/food/confirm_order.md) once to finalize (the backend marks it failed if payment is still non-terminal; a late success reconciles server-side): ```ts await client.callTool({ name: "confirm_order", // Food: orderId + addressId + lat + lng (no paasId). IM/Dineout/Scenes: orderId + paasId. arguments: { orderId: order.data.orderId, addressId: home.id, lat: home.lat, lng: home.lng }, }); ``` ## Step 5 - Track Same as the COD journeys: [`track_food_order`](https://mcp.swiggy.com/builders/docs/reference/food/track_food_order.md) / [`track_order`](https://mcp.swiggy.com/builders/docs/reference/instamart/track_order.md) / [`get_booking_status`](https://mcp.swiggy.com/builders/docs/reference/dineout/get_booking_status.md) / [`get_event_booking_status`](https://mcp.swiggy.com/builders/docs/reference/scenes/get_event_booking_status.md). ## Non-UI (headless) clients Everything above assumes a **widget host** — the picker and confirmation widgets render the QR (or launch the UPI app), auto-poll `check_payment_status`, and auto-confirm. **Headless clients** — terminal MCP clients, server-to-server agents, anything that can't render a widget — run the *same four tools*. They just present options as text, share a payment link, and drive the poll loop themselves. **Both payment methods are fully supported without any UI:** | Method | How it works headless | | --- | --- | | **Cash (COD)** | No payment leg at all — place with `paymentMethod:"Cash"` and you're done. The truly zero-UI path. | | **UPI** (app intent *and* scan-QR) | The place-order response returns a ready-to-use **`bridgeUrl`** — an opaque HTTPS payment link — in **`data.bridgeUrl`** (present for *both* the app-intent and scan-QR choices, which resolve to the **same** link). Read it from `data.bridgeUrl` and share it with the user. **Opening it renders a scan-or-tap payment page** — a scannable QR plus an "Open in your UPI app" button — so the user **scans it on desktop, or taps to open their UPI app on mobile**; you don't need to render your own QR. The app-intent flow also relays the link in the response `message`; the scan-QR flow does not — so always read `data.bridgeUrl` rather than parsing the message. | ### The headless flow ``` get_payment_options ──► read data.allMethods (flat text list) ──► present as text; user picks │ ▼ place-order (paymentMethod:"UPI" + intentApp | generateUPIQR OR paymentMethod:"Cash") │ UPI → response carries: bridgeUrl, paasId, orderId, isQrFlow, │ pollingIntervalInMs, maxTimeToPollForInMs ▼ share bridgeUrl with the user (opens a scan-or-tap QR page: scan on desktop, tap on mobile) │ ▼ check_payment_status ──► YOUR capped loop (honor maxTimeToPollForInMs; re-poll gently on pending) │ ▼ confirm_order ──► call it yourself on SUCCESS, or once at your cap if still pending │ ▼ track-order ``` Three behavioural differences from the widget path: **(1)** you present `data.allMethods` as text instead of a rendered picker, **(2)** you hand the user `data.bridgeUrl` (opening it shows a scan-or-tap QR page) instead of an in-widget QR / tap-button, and **(3)** nothing auto-polls or auto-confirms — you own the `check_payment_status` cadence and the final `confirm_order`. ```ts // 1. Options as text (no picker widget) const opts = (await client.callTool({ name: "get_payment_options", arguments: { addressId: home.id }, // Food only })).data; // opts.allMethods → flat list you can print; opts.cod → Cash fallback // 2. Place the order with the user's pick (UPI shown; for Cash use paymentMethod:"Cash") const order = (await client.callTool({ name: "place_food_order", arguments: { addressId: home.id, paymentMethod: "UPI", intentApp: picked.id }, })).data; // Cash has no payment leg — the order is already placed. Skip straight to tracking. if (picked.paymentMethod === "Cash") { // → track-order; nothing more to do (no bridgeUrl, no polling). } else { // 3. UPI → share the payment link (opens a scan-or-tap QR page). console.log(`Pay here — scan on desktop, tap on mobile: ${order.bridgeUrl}`); // 4. Drive your OWN capped poll — the widget isn't here to do it. // Food: ALSO echo addressId + lat + lng, or the server can't reconcile the order → stuck pending. const deadline = Date.now() + order.maxTimeToPollForInMs; let s; do { s = (await client.callTool({ name: "check_payment_status", arguments: { paasId: order.paasId, orderId: order.orderId, addressId: home.id, lat: home.lat, lng: home.lng }, })).data; if (s.terminal) break; await sleep(order.pollingIntervalInMs); // gentle — it's a ~19s long-poll } while (Date.now() < deadline); // 5. Finalize yourself if (s.status === "success" || s.status === "paid") { if (!s.confirmed) { // usually already confirmed server-side; confirm if not await client.callTool({ name: "confirm_order", arguments: { orderId: order.orderId, addressId: home.id, lat: home.lat, lng: home.lng }, }); } } else if (!s.terminal) { // hit the cap still pending → confirm once to finalize (a late success reconciles server-side) await client.callTool({ name: "confirm_order", arguments: { orderId: order.orderId, addressId: home.id, lat: home.lat, lng: home.lng }, }); } } ``` > **Note** > > Headless agent prompt: when the user is ready to pay, call `get_payment_options` and present `data.allMethods` as a short text list — never ask which device they're on, never ask for a UPI ID. On a UPI pick, place the order and give the user `data.bridgeUrl` to open on their phone. Then poll `check_payment_status` every `data.pollingIntervalInMs`, capped at `data.maxTimeToPollForInMs`: on `success` the order is usually already placed, on `failed` offer `get_payment_options` again, on `refund-initiated` stop, and if you hit the cap still `pending` call `confirm_order` once. For Cash, just place with `paymentMethod:"Cash"` — no link, no polling. Cash is never offered on Scenes. ## Per-server contract The Payment stage is shared, but each server echoes the identifiers its place-order and confirm need. Echo them from the responses - don't reconstruct them. | Server | Place-order tool | UPI args | `confirm_order` args | | --- | --- | --- | --- | | **Food** | `place_food_order` | `addressId` + `paymentMethod:"UPI"` + `intentApp` / `generateUPIQR` | `orderId` + `addressId` + `lat` + `lng` | | **Instamart** | `checkout` | `paymentMethod:"UPI"` + `intentApp` / `generateUPIQR` | `orderId` + `paasId` | | **Dineout** | `book_table` (paid deal) | `cartKey` + `paymentMethod:"UPI"` + `intentApp` / `generateUPIQR` | `orderId` + `paasId` | | **Scenes** | `place_event_order` | `cartKey` + `paymentMethod:"UPI"` + `intentApp` / `generateUPIQR` | `orderId` + `paasId` | Dineout's paid path also needs `create_cart` (`DEAL_TICKET_PURCHASE`) first, which returns the `cartKey`. Scenes needs `create_event_cart` first, and only enters this stage when it returns `checkoutMode=UPI`. ## Full agent prompt > **Note** > > When the user is ready to pay, call `get_payment_options` - it renders the picker; never ask which device they're on, and never ask for a UPI ID. Pass the picked method into the place-order tool exactly (`intentApp` for an app, `generateUPIQR` for desktop QR). The order is created in PENDING_PAYMENT; the widget polls `check_payment_status` and finalizes automatically, so do NOT loop it yourself. On `failed`, offer to retry via `get_payment_options`; on `refund-initiated`, tell the user a refund is underway and stop. If UPI isn't available, the picker offers Cash - use COD. On Scenes there is no Cash option, so stop and tell the user the ticket cannot be paid for right now. ## What can go wrong - **UPI not available** → the picker shows only Cash; fall back to COD (`paymentMethod:"Cash"`). - **Payment `failed`** → do NOT call `confirm_order`; re-show `get_payment_options` and place again (a fresh transaction on the same cart). - **`refund-initiated`** → the money was debited and a refund has already started; tell the user and do not retry. - **Timed out** (still `pending` at your cap) → call `confirm_order` once to finalize; a late payment success is reconciled server-side. - **Tight-looping `check_payment_status`** → it's a long-poll; hammering it stresses the payment cache. Let the widget own the cadence, or cap your own loop. - **NPCI compliance** → never ask the user for their UPI ID (VPA); the picker and QR handle it. UPI Collect and saved VPAs are not surfaced. --- ### Ship to production Retries, observability, and the go-live checklist. You've built it. Before flipping production traffic on, here's the punch list. ## Errors and retries ### Idempotency by tool class | Class | Examples | Safe to retry on failure? | | --- | --- | --- | | **Pure reads** | `get_addresses`, `search_restaurants`, `get_order_details` | Always | | **Cart mutations** | `update_cart`, `update_food_cart`, `clear_cart`, `flush_food_cart` | Yes - server is idempotent on session; retrying the same args won't double-add | | **Order placement** | `place_food_order`, `checkout`, `book_table` | **No**, not by default - see below | | **Coupon application** | `apply_food_coupon` | Yes | | **Tracking** | `track_order`, `track_food_order`, `get_booking_status` | Always | ### Order placement (non-idempotent) `place_food_order`, `checkout`, and `book_table` are **not safe to blind-retry** on network failure. Instead: 1. On 5xx or network error, wait 2-5 seconds. 2. Call `get_food_orders` / `get_orders` / `get_booking_status` to check if the order actually went through. 3. If yes, treat the original failure as success. 4. If no, retry the original call. A future `Idempotency-Key` header will close this gap; the check-then-retry pattern works today. ### Exponential backoff For retriable failures (generic 5xx and upstream timeouts / errors - symbolic codes `UPSTREAM_TIMEOUT` / `UPSTREAM_ERROR` / `INTERNAL_ERROR` ship once the error-code registry is live): ```ts async function retry(fn: () => Promise, maxAttempts = 4): Promise { let attempt = 0; while (true) { try { return await fn(); } catch (e: any) { attempt++; if (attempt >= maxAttempts) throw e; if (!isRetryable(e)) throw e; const baseMs = 500 * 2 ** (attempt - 1); // 500, 1000, 2000, 4000 const jitterMs = Math.random() * baseMs * 0.3; await new Promise((r) => setTimeout(r, baseMs + jitterMs)); } } } function isRetryable(e: any) { const status = e?.status ?? e?.response?.status; if (status && status >= 500 && status < 600) return true; const code = e?.body?.error?.code; return ["UPSTREAM_TIMEOUT", "UPSTREAM_ERROR", "INTERNAL_ERROR"].includes(code); } ``` ### Rate-limited (planned: 429) MCP-layer rate limiting is not enforced in v1.0 - you will not see `429 Too Many Requests` from a Swiggy MCP endpoint today (see [rate-limits](https://mcp.swiggy.com/builders/docs/operate/rate-limits.md)). Wire the handler so you're ready when v1.1 ships it: ```ts if (response.status === 429) { const seconds = Number(response.headers.get("Retry-After") ?? 30); await new Promise((r) => setTimeout(r, seconds * 1000)); return retry(); } ``` Once the header ships, always honour `Retry-After` - don't stack it on top of exponential backoff. ### Auth failures (401) Re-run the OAuth flow. Never retry with the same token. Auth errors also surface via JSON-RPC `-32001` at the transport layer. ```ts if (response.status === 401) { await reAuthenticate(); return retry(); } ``` ### Retry budget For user-facing flows: **cap total retries at 30 seconds of wall-clock time.** Beyond that, fail loudly and let the user decide. ## Observability ### The session id is your friend Every tool call is tagged with a session id that flows across Swiggy's services. On your side, log it. When you file a support ticket, include the session id of the failing call - we can trace the full request path end-to-end in seconds. ### Recommended client-side log shape ```json { "ts": "2026-04-17T10:00:00Z", "level": "info", "event": "mcp_tool_call", "tool": "search_restaurants", "user_id_hash": "sha256:...", "session_id": "...", "duration_ms": 217, "status": "ok" } ``` Hash user IDs at rest unless you have a specific DPDP-compliant reason to store them plaintext. See [data-and-compliance](https://mcp.swiggy.com/builders/docs/operate/data-and-compliance.md). ### Metrics to track client-side | Metric | Why | | --- | --- | | Tool call latency (p50, p95, p99) | Surface regressions before users notice | | Tool call success rate | Catches partial outages | | 4xx / 5xx rate | Separates client bugs from server bugs | | OAuth refresh frequency | Unusually high = token-management bug | ### OpenTelemetry If your platform supports OpenTelemetry, instrument every `callTool` with a span. Tag with the session id. When you cross-reference with Swiggy support, we'll be on the same timeline. ### What Swiggy exposes - `status.swiggy.com/mcp` - public status page (shipping in v1.1). - Incident comms via email to your designated engineering contact. - Per-partner usage dashboards for enterprise integrators. ### What's not available - Raw server logs (security / privacy). - Infra dashboards. - User-level audit logs (only on lawful request - see [data-and-compliance](https://mcp.swiggy.com/builders/docs/operate/data-and-compliance.md)). ## Go-live checklist Before your first real-user traffic: - [ ] **Access**: staging has been green for ≥ 48 hours and production access has been confirmed by [builders@swiggy.in](mailto:builders@swiggy.in). - [ ] **Redirect URIs**: every URL your OAuth flow might redirect to is allowlisted (exact-match). - [ ] **Servers**: you've confirmed which Swiggy MCP servers your integration calls (`food`, `instamart`, `dineout`, `scenes`); v1 scopes (`mcp:tools`/`mcp:resources`/`mcp:prompts`) are requested uniformly. - [ ] **Error handling**: retry logic per this page is in place; auth failures (401 / JSON-RPC `-32001`) and upstream timeouts each have an explicit branch. - [ ] **Idempotency guards**: order-placement paths do check-then-retry, not blind retry. - [ ] **Cart confirmation**: no order is placed without user-visible confirmation of items + total. - [ ] **Rate limits**: you've benchmarked your expected QPS and confirmed you're under the ceiling ([rate-limits](https://mcp.swiggy.com/builders/docs/operate/rate-limits.md)). - [ ] **Observability**: session id logged on every call; metrics exported to your usual observability stack. - [ ] **Deprecation monitoring**: alerting set up on the `_meta.swiggy.deprecation` field so you won't miss a breaking change. - [ ] **Incident contact**: Swiggy has an email + Slack channel to reach your on-call for S0/S1. - [ ] **Data handling**: your product's data retention, deletion, and consent flows align with [data-and-compliance](https://mcp.swiggy.com/builders/docs/operate/data-and-compliance.md). - [ ] **Voice vs chat**: if you have a voice surface, your prompts are shaped per [voice-vs-chat](https://mcp.swiggy.com/builders/docs/build/agent-patterns/voice-vs-chat.md). - [ ] **Support runbook**: you've written an internal runbook for "what do we do when Swiggy returns X". - [ ] **Rollout**: traffic ramps 1% → 10% → 50% → 100% over at least 24 hours so any regression is caught early. ## When to escalate If production traffic is failing and you've already: 1. Verified tokens are fresh and scopes match. 2. Confirmed you're hitting the right endpoints. 3. Seen a sudden error-rate spike on Swiggy's side (not yours). ...mail [builders@swiggy.in](mailto:builders@swiggy.in) with the failing session ids and timestamps. Enterprise partners use their designated engineering contact + SEV channel. --- ### Widgets Render-ready UI fragments that Swiggy MCP servers can return alongside tool responses. Not every AI client wants to render restaurant cards, menu items, and carts from scratch. Widgets let Swiggy ship pre-built UI fragments the client can embed directly. > **Warning** > > The widget iframe/postMessage contract described below is the **designed surface** - the Food server flags `hasWidgets: true` and the internal registry is in place, but the public `https://mcp.swiggy.com/widgets/...` iframe URLs, the `X-Swiggy-Widgets` opt-in header, and the `?theme=` query param are not live in v1.0. If you're prototyping a chat client today, wire the integration per this contract and expect `widgets` to populate once the hosting layer ships. The `data` envelope carries everything semantically needed even without widgets. ## What a widget is An **iframe-embeddable HTML document** returned alongside the tool response: ```json { "success": true, "data": { /* normal tool payload */ }, "widgets": [ { "id": "food-restaurant-card-abc", "type": "restaurant-card", "src": "https://mcp.swiggy.com/widgets/food/restaurant-card?id=res_123" } ] } ``` Your client renders each widget in an iframe sized to the widget type. A postMessage bridge lets the widget send events back ("user clicked add-to-cart", "user bumped quantity", etc). ## Availability | Server | v1.0 | Planned | | --- | --- | --- | | Food | Registry flagged `hasWidgets: true`; hosted iframe layer in progress | restaurant-card, menu-item, cart-widget GA | | Instamart | - | product-card, cart-widget | | Dineout | - | restaurant-card, slot-picker | | Scenes | - | event-card, show-picker | If you use voice or TTS surfaces, widgets are useless - stick to text responses. Widgets are for chat surfaces (Claude, ChatGPT, Cursor, custom web chat). ## Opt-in (planned) Once the hosting layer ships, the opt-in will be the header `X-Swiggy-Widgets: enabled` on your tool calls. Without it, responses will omit the `widgets` array. Not wired in v1.0. ## Food widgets (planned) ### `restaurant-card` Returned alongside: `search_restaurants`, `get_restaurant_menu` | Field | Value | | --- | --- | | Purpose | Single restaurant with photo, rating, delivery time, "View menu" CTA | | Iframe size | `width: 100%; max-width: 420px; height: 180px` | | postMessage events | `restaurant-card.clicked`, `restaurant-card.menu-requested` | ### `menu-item` Returned alongside: `get_restaurant_menu`, `search_menu` | Field | Value | | --- | --- | | Purpose | Single menu item with photo, description, price, variant picker, "Add to cart" CTA | | Iframe size | `width: 100%; max-width: 420px; height: 240px` | | postMessage events | `menu-item.add-to-cart` with `{ itemId, variantId, addOns[] }` | ### `cart-widget` Returned alongside: `get_food_cart`, `update_food_cart` | Field | Value | | --- | --- | | Purpose | Current cart: items, subtotal, delivery fee, total, "Checkout" CTA | | Iframe size | `width: 100%; max-width: 480px; height: 320px` | | postMessage events | `cart.item-removed`, `cart.quantity-changed`, `cart.checkout-requested` | ## Embedding pattern ```tsx function SwiggyWidget({ widget }: { widget: Widget }) { return (