Complete Booking Flow
The ONDC booking flow is strictly sequential. Each step depends on values returned by the previous step. This guide shows the complete path from browsing to ticket confirmation.
Critical: Never skip or reorder steps. The
transaction_id obtained in SELECT must remain unchanged across SELECT → INIT → CREATE ORDER. It uniquely ties together your entire booking session.
Phase 1 — Setup (One-time)
These APIs are typically called once on app load.
-
1Get Token POST /auth/tokenAuthenticate with clientId + clientSecret → Save
access_tokenas your bearer token. Valid for 30 min. -
2Get the user's city. Save
city.code(e.g.,std:011) as cityCode — used in all ONDC transaction APIs. -
3Check enabled payment gateways for the platform/payment-options UI. Load city banners for the home screen.
Phase 2 — Browse & Select Experience
-
4Show paginated experience listings. Use filters (category, city, price, etc.) or autocomplete search.
-
5Get Single Item GET /item/slug/:idFull item detail page. Save these critical values:
data.key(MongoDB ObjectId) → needed for timeslots
provider.bppId,provider.bppUri,provider.cityCode,provider.id→ needed for all ONDC transactions
ticketsByPackage[n].packageId→ needed for tickets API
ticketsByPackage[n].tickets[n].key→ needed for timeslots
bookableDates→ restrict date picker to only these dates
Phase 3 — Booking Transaction
This is the core ONDC flow. All steps are mandatory and must be executed in order.
-
6Get Timeslots GET /item/:itemKey/timeslotsAfter user picks a date from
bookableDates. Use item's MongoDBkey(not UUID). Returnstimeslot.id,timeRangeStart,timeRangeEnd— all needed for SELECT. -
7Get Tickets GET /item/:packageId/ticketsPass
packageIdandtimeStamp(the timeslot start). Returns liveticket.id(composite format — use as-is),parentItemId,minPeople/maxPeople,fulfillmentId. -
8SELECT POST /selectSend items + fulfillment to the seller (BPP). Returns confirmed price quote and optional visitor forms. SAVE
transaction_id— must be used unchanged in all subsequent steps. -
8bForm Submit (CONDITIONAL)Only if SELECT returns a non-empty
forms[]array. Submit visitor details (name, age, email). Submit once per ticket if booking multiple. Proceed to INIT only after all forms are submitted. -
9INIT POST /initAdd billing details (name, email, mobile). Use the same
items[]+fulfillments[]from SELECT unchanged. Returnspayments[0].id(paymentId) needed for Create Order. -
10Create Order POST /ordersCreates the payment order. Returns
orderIdandrpOrderId. UserpOrderIdwith the Razorpay SDK to launch the checkout modal. -
PPayment (Razorpay SDK)User completes payment via Razorpay checkout. On success, the Razorpay webhook automatically triggers CONFIRM. You do NOT call CONFIRM manually.
-
11Poll Order Status GET /orders/:orderIdPoll every 2–3 seconds until
orderStatus = SUCCESSANDbookingStatus = COMPLETED. Maximum 30 attempts. Show "Processing…" after 60s if still pending.
Phase 4 — Post Booking
-
12Show the user's ticket with QR code. Render
qrCodeDetails.tokenas a QR image. CheckqrCodeDetails.status(UNCLAIMED / CLAIMED) for scan state. -
13Create Review (optional)Allow review only when
bookingStatus = COMPLETEDand no existing review. RequiresbookingIdfrom the tickets response. -
14Register Issue (if problem)If the user has a problem (QR not working, experience not delivered), raise an ONDC IGM issue. Use
transactionIdfrom the booking.
Field Traceability
Every critical value and where it comes from / where it's used:
| Value | Comes From | Used In |
|---|---|---|
| bearer token | POST /auth/token → access_token | Authorization header on all APIs |
| cityCode | GET /city → code or item → provider.cityCode | SELECT, INIT → cityCode |
| bppId | GET /item/slug → provider.bppId | SELECT, INIT → bppId |
| bppUri | GET /item/slug → provider.bppUri | SELECT, INIT → bppUri |
| providerId | GET /item/slug → provider.id | SELECT, INIT → providerId |
| itemKey (ObjectId) | GET /item/slug → data.key | GET /timeslots → path param |
| ticketKey (ObjectId) | GET /item/slug → ticketsByPackage[n].tickets[n].key | GET /timeslots → ticketIds query |
| timeslot.id | GET /timeslots → data[n].id | SELECT → fulfillments[n].id + items[n].fulfillment_ids |
| ticket.id (composite) | GET /tickets → data[n].id | SELECT → items[n].id (use whole string) |
| ticket.parentItemId | GET /tickets → data[n].parentItemId | SELECT → items[n].parent_item_id |
| transaction_id | SELECT → onSelect.context.transaction_id | INIT, CREATE ORDER → transactionId |
| paymentId | INIT → message.order.payments[0].id | CREATE ORDER → paymentId |
| orderId | CREATE ORDER → data.orderId | GET /orders/:orderId (polling) |
| rpOrderId | CREATE ORDER → data.rpOrderId | Razorpay SDK → order_id |
| bookingId | GET /user/tickets → formattedTickets[n].bookingId | CREATE REVIEW → bookingId |
| cityId (ObjectId) | GET /city → data.cities[n].id | GET /hero-sections/city/:id |