Booking Flow

The complete ONDC transaction lifecycle — SELECT → Form Submit → INIT → Create Order → Confirm. All steps are sequential and each depends on values from the previous.

Critical: The transaction_id returned by SELECT must be passed unchanged to INIT and Create Order. Do not generate a new one. See the Complete Flow Guide for the full picture.

# Select

First step of the ONDC transaction. Sends your selected items and fulfillment slot to the provider's BPP. Returns the confirmed price quote, cancellation terms, and optional visitor detail forms.

POST{{base_url}}/select
Bearer Token Required
Request Body — application/json
FieldTypeRequiredSource
cityCodestringREQUIREDGET /item/slug: provider.cityCode
bppIdstringREQUIREDGET /item/slug: provider.bppId
bppUristringREQUIREDGET /item/slug: provider.bppUri
providerIdstringREQUIREDGET /item/slug: provider.id
itemsarrayREQUIREDSee structure below
fulfillmentsarrayREQUIREDSee structure below
items[n] Structure
{
  "id": "019be49d-..._9000.00_1000",    // GET /tickets: data[n].id (composite — use as-is)
  "parent_item_id": "019be49c-...", // GET /tickets: data[n].parentItemId
  "quantity": {
    "selected": { "count": 2 }      // User-selected quantity
  },
  "fulfillment_ids": ["019be49e-48f1-71c0-..."]  // GET /timeslots: data[n].id
}
fulfillments[n] Structure
{
  "id": "019be49e-48f1-71c0-89c6-a0aae9aa90b9",  // GET /timeslots: data[n].id
  "stops": [{
    "type": "START",
    "time": {
      "range": {
        "start": "2026-02-28T03:30:00.000Z",  // GET /timeslots: timeRangeStart
        "end":   "2026-02-28T12:30:00.000Z"   // GET /timeslots: timeRangeEnd
      }
    }
  }]
}
Response — 201 Created (key fields)
{
  "data": {
    "onSelect": {
      "context": {
        "transaction_id": "1ca40299-27bc-4bf9-b6a0-4095190ab1ab"  // ← SAVE THIS! Pass to INIT + Create Order unchanged
      },
      "message": {
        "order": {
          "quote": {
            "breakup": [
              { "title": "BASE_PRICE", "price": { "value": "18000.00" } },
              { "title": "TAX",        "price": { "value": "0" } }
            ],
            "price": { "currency": "INR", "value": "18000.00" }  // Final amount to display
          }
        }
      }
    },
    "forms": [...]  // If non-empty → call Form Submit before INIT
  }
}
Save transaction_id! This ID ties your entire booking session together. It must be passed unchanged to INIT and Create Order. If you lose it, start a new SELECT.
If data.forms is non-empty, you must call Form Submit before proceeding to INIT.

# Form Submit (Conditional)

Submit additional visitor details required by certain providers (e.g., ASI monuments need visitor name, age, nationality). Only call this if SELECT returns a non-empty forms[] array.

POST{{base_url}}/form/group/:groupId
No Auth Required
Path Parameters
FieldTypeRequiredSource
groupIdstringREQUIREDSELECT response: data.forms[n].groupId
Request Body — application/json
FieldTypeRequiredDescription
transactionIdstringREQUIREDFrom SELECT: onSelect.context.transaction_id
data.formIdstringREQUIREDHidden field — SELECT: forms[n].fields where name="formId"
data.itemIdstringREQUIREDHidden field — SELECT: forms[n].fields where name="itemId"
data.namestringREQUIREDVisitor's full name
data.emailstringREQUIREDVisitor's email address
data.mobilestringREQUIRED10-digit mobile number
data.agestringCONDITIONALRequired if form has minimum age constraint
data.countrystringOPTIONALDefault: India
data.genderstringOPTIONALMale / Female / Other
data.countrycodestringOPTIONALPhone country code. Example: +91
Multiple Submissions: Check forms[n].totalSubmissionsRequired. If the user booked 2 tickets, submit the form twice with different visitor details. Proceed to INIT only when the response forms[] array is empty.
Hidden fields: Fields with "type": "hidden" must be included in the data payload but should NOT be shown in the UI. Include formId and itemId exactly as returned.

# Init

Initializes the order with billing details. Returns the final payment amount, a payment ID, and BPP settlement terms. Must be called after SELECT (and Form Submit if required).

POST{{base_url}}/init
Bearer Token Required
Request Body — application/json
FieldTypeRequiredSource
cityCodestringREQUIREDSame cityCode used in SELECT
transactionIdstringREQUIREDSELECT → onSelect.context.transaction_idmust be identical to SELECT
bppIdstringREQUIREDSame bppId from SELECT
bppUristringREQUIREDSame bppUri from SELECT
providerIdstringREQUIREDSame providerId from SELECT
itemsarrayREQUIREDSame items[] array from SELECT — unchanged
fulfillmentsarrayREQUIREDSame fulfillments[] array from SELECT — unchanged
userNamestringREQUIREDBilling user's full name
userEmailstringREQUIREDBilling user's email address
userMobilestringREQUIRED10-digit mobile number (no country code)
attractionIdstringOPTIONALItem UUID from GET /item/slug: data.id — for analytics tracking
Response — 201 Created (key fields to save)
{
  "data": {
    "context": { "transaction_id": "1ca40299-..." },
    "message": {
      "order": {
        "quote": {
          "price": { "currency": "INR", "value": "9000.00" }  // Final amount to display
        },
        "billing": { "name": "...", "email": "...", "phone": "+91-..." },
        "payments": [
          {
            "id": "61ede945-ebf6-4389-a92f-86763580e906",  // ← SAVE: Create Order → paymentId
            "status": "NOT-PAID",   // Expected at this stage
            "type": "PRE-ORDER"
          }
        ],
        "tags": [
          {
            "descriptor": { "code": "BPP_TERMS" },  // ← SAVE entire object: Create Order → bppTerms
            "list": [...]
          }
        ]
      }
    }
  }
}
Values to Save from Response
Response PathUsed In
message.order.payments[0].idCreate Order → paymentId
message.order.quote.price.valueDisplay final amount to user before payment
message.order.tags (BPP_TERMS)Create Order → bppTerms
Payment status "NOT-PAID" in the response is expected — payment happens in the next step.

# Create Order

Creates a payment order in the platform. Returns the Razorpay order ID (rpOrderId) needed to launch the payment gateway on the frontend.

POST{{base_url}}/orders
Bearer Token Required
Request Body — application/json
FieldTypeRequiredDescription
devicestringREQUIREDPlatform: WEB, ANDROID, IOS
transactionIdstringREQUIREDSame transaction_id from SELECT/INIT (unchanged)
pgstringREQUIREDPayment gateway: RAZORPAY. Check GET /pg-config for available options.
userIdstringOPTIONALLogged-in user's UUID from JWT token
Response — 201 Created
{
  "data": {
    "orderId": "HDBAP0000000166",       // ← SAVE: poll GET /orders/:orderId for booking status
    "pg": "RAZORPAY",
    "rpOrderId": "order_SUbGfZnuak5rGb",  // ← Pass to Razorpay SDK to open checkout modal
    "pgOrderId": "order_SUbGfZnuak5rGb",
    "orderStatus": "INITIATED"           // Expected at this stage
  }
}
Opening Razorpay Checkout
// Example Razorpay SDK usage
var rzp = new Razorpay({
  key: "<RAZORPAY_KEY_ID>",
  order_id: "order_SUbGfZnuak5rGb",  // rpOrderId from response
  amount: 900000,                   // amount in paise (9000 INR × 100)
  currency: "INR",
  name: "Entry Pass",
  handler: function(response) {
    // Payment success — now poll GET /orders/:orderId
  }
});
rzp.open();
Do NOT call CONFIRM manually. After successful payment, the Razorpay webhook automatically triggers the ONDC CONFIRM flow. Your job is simply to poll GET /orders/:orderId to check the result.

# Get Order Status

Poll the booking status after payment completes. Since ONDC CONFIRM is asynchronous (BPP may take time to respond), keep polling until both orderStatus = SUCCESS and bookingStatus = COMPLETED.

GET{{base_url}}/orders/:orderId
Bearer Token Required
Path Parameters
FieldTypeRequiredDescription
orderIdstringREQUIREDHD platform order ID from Create Order response. Example: HDBAP0000000166
Query Parameters
FieldTypeRequiredDescription
userIdstringOPTIONALUser UUID for ownership verification
userMobilestringOPTIONALMobile number for ownership verification
Response — 200 OK
{
  "data": {
    "orderId": "HDBAP0000000025",
    "orderStatus": "SUCCESS",    // "INITIATED" | "SUCCESS" | "FAILED"
    "bookingStatus": "COMPLETED"  // null | "PENDING" | "COMPLETED" | "EXPIRED" | "CANCELLED"
  }
}
Polling Logic
ConditionAction
orderStatus = SUCCESS + bookingStatus = COMPLETED✅ Booking confirmed — navigate to ticket page
orderStatus = INITIATED⏳ Payment pending — keep polling (max 30× every 2–3s)
bookingStatus = null after 60s⏳ Show "Processing…" — BPP is slow to respond
orderStatus = FAILED❌ Payment failed — show retry option
bookingStatus = CANCELLED❌ Cancelled by provider — show error