The booking flow
Four steps, identical in every vertical:
Only step 1 differs per vertical. Steps 2–4 are the same call for everything you sell.
1 · Discover
Each vertical has its own discovery endpoint, because a coach seat and a VIP subscription are not searched the same way:
| Vertical | Discovery |
|---|---|
| Airport transfer | POST /api/v1/airport-transfer/search |
| Express bus | POST /api/v1/express-bus/search |
| Entertainment | GET /api/v1/entertainment/catalog |
Entitlement filters discovery silently — inventory you are not contracted to sell simply does not appear. An empty result is normal and is not an error.
2 · Quote
Discovery mints a price-locked quoteId on every row. You do not call a separate quote endpoint;
you choose a row and carry its quoteId forward. See Quotes for expiry rules.
3 · Book
One vertical-agnostic call:
POST /api/v1/bookings
{
"idempotencyKey": "a1f4c2e0-77b2-4c31-9c2e-6b1f0a9d3e55",
"items": [
{ "quoteId": 90188, "details": { /* per-vertical */ } }
]
}
idempotencyKey— yours to choose, unique per attempt. Re-sending it replays the original order instead of booking twice.quoteId— send it unquoted. A string or a UUID is rejected.details— the per-vertical bag. Which shape applies is decided by the offering behind the quote, not by anything you set. See the vertical pages for the exact fields.
You never send a price. Net and sell both come from the frozen quote.
A round trip is two items
items is a list, and every entry needs its own quoteId from its own search — a quote cannot
be reused. Two entries book atomically: both confirm, or both compensate.
{
"idempotencyKey": "7b3e9a01-4c22-4d3a-9e88-1f2a3b4c5d6e",
"items": [
{ "quoteId": 90188, "details": { "...": "outbound" } },
{ "quoteId": 90189, "details": { "...": "inbound" } }
]
}
This is the whole story for multi-leg orders. There is no round-trip endpoint and no package endpoint — the item list is the mechanism.
4 · Read back
GET /api/v1/bookings/{id} returns the same envelope at any time. Use it to poll an order whose
outcome was not yet decided, rather than booking again.
The response envelope
Identical for every supplier and vertical:
{
"orderId": "b3f1c2d4-9a8b-4c7d-8e6f-1a2b3c4d5e6f",
"referenceNo": "GHB1723600000000K7Q2",
"state": "CONFIRMED",
"items": [
{
"itemId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"referenceNo": "GHB1723600000000K7Q2-1",
"state": "CONFIRMED",
"supplierBookingRef": "TB-100234",
"redemptionRef": "TC-9",
"sellAmount": 48.00,
"currency": "MYR",
"detail": { "type": "TAXI_COUPON", "coupons": [ /* ... */ ] }
}
],
"payable": 48.00,
"currency": "MYR",
"charges": []
}
| Field | What it is for |
|---|---|
orderId | The UUID. Use it to read the order back. |
referenceNo | The short handle. Quote this to GoHub support. |
state | Where the order rests — see Outcomes. |
items[].redemptionRef | What the customer redeems, where there is such a thing. |
items[].detail | The supplier-specific bag, discriminated by its own type. |
payable / charges | Your commercial view: what your account is charged, and the fee lines behind it. |
detail is the only part of the envelope that varies by supplier. Switch on detail.type to
render a coupon QR, a boarding code, or nothing — and the envelope around it will not change when
we add a supplier.
payable and charges are returned only to the agent that owns the order. Another agent reading
the same id gets null and an empty list.