Neptune Flight Booking API
A clean B2B flight booking API: search → fare rules → create booking → passengers → reprice → book. One contract, sensible defaults, predictable responses.
Already have credentials? Sign in to issue real bookings from the browser — no curl required. New here? Ask your admin to bind a User row to your Partner in Django admin.
This documentation covers every endpoint a partner integration needs, from authentication through to booking confirmation. The complete flow uses 8 endpoints; you can run a working search-to-book sequence in under a minute once your API key is set up.
Base URL
https://dev.neptune.tarzanway.comSandbox and production hosts are issued separately as part of partner onboarding.
What this API gives you
- Universal contract. One response shape across one-way, round-trip, domestic, and international.
- Stable identifiers. Once a booking is created you get a permanent
booking_idyou can reference forever. - Synchronous booking. No webhooks or polling —
bookreturns the PNR in the response. - Prepaid billing. Top up your wallet in advance; bookings deduct from it automatically and your live balance is always visible.
Partner Portal
Prefer a UI over the API? Your account ships with a hosted partner portal — a browser app for searching, booking, and managing travel without writing a single line of code. Same backend, same booking lifecycle, same wallet, one login across both verticals.
Authentication
Every request must include your API key in the Authorization header. Requests without a valid key receive 401 Unauthorized.
Authorization: Api-Key <your_raw_key>
Sample authenticated request
curl -X POST https://dev.neptune.tarzanway.com/v1/flights/search/ \
-H "Authorization: Api-Key YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{ ... }'
Quick Start
Five steps from zero to a confirmed PNR:
- Search for flights → grab
trace_id+ aresult_index - Fare Rules (optional) → review cancellation policy
- Create Booking → lock fares, get
booking_id - Save Passengers → submit traveller details
- Reprice (optional) → confirm fare hasn't moved
- Book → issue PNR
book can be repeated. A trace_id expires after 15 minutes; a booking_id remains addressable indefinitely.Try in Postman
A ready-to-run Postman collection covers every endpoint with sample requests, example responses, and auto-chaining test scripts (you click through the requests in order; trace_id / booking_id are captured for you).
base_url + api_key placeholdersHow to use
- Download both files above.
- Open Postman → Import → drop both JSONs.
- Select the imported environment Neptune Flights — Sandbox from the environment dropdown (top-right of Postman).
- Edit the environment and set
api_keyto the key Neptune shared with you. Thebase_urlis preconfigured tohttps://dev.neptune.tarzanway.com. - Open
01. Flight Searchand click Send. The test script saves thetrace_idand chosenresult_indexautomatically. Walk through the remaining requests in order.
Suppliers
Two suppliers sit behind the same API. Pass source on /v1/flights/search/ to choose; it defaults to Nuitee.
source | Supplier | What to know |
|---|---|---|
nuitee (default) | Nuitee Connect | Offers expire in ~5–10 minutes. Travel documents required on every passenger. Round trips are always sold as a combination. |
travclan | TravClan | Flat 15-minute result window. Travel documents required on international routes only. Round trips are combinations internationally, independent lists domestically. |
You only send source on the search. Every later call in the chain is pinned to whichever supplier served that search — we resolve it from the trace_id and from the booking itself. A booking never falls back to the other supplier: a trace_id, its result indices and its fares are all supplier-bound.
Differences that affect your integration
- Round-trip shape. Read
round_trip_styleon the search response."independent"means separateoutbound[]andinbound[]lists — send both result indices to create-booking."coupled"means the returns are nested under each outbound asinbound_options[]— send only the chosen option's result index. Nuitee is always coupled; TravClan is coupled internationally and independent domestically. - Result lifetime. Use
expires_at/remaining_secondswhen present instead of assuming 15 minutes. - Passenger documents. Read
passport_requiredon the booking. Nuitee rejects a booking without passport number, expiry and nationality on every passenger, domestic sectors included — do not derive this fromis_domestic. - SSR. Nuitee has no meal ancillary, and seats/baggage are only offered after a booking is reserved, so the
ssrblock on a passenger is ignored for Nuitee bookings. - Duplicate detection. Only TravClan reports it. For Nuitee,
duplicates_foundis alwaysfalse. - Expired handles. If the fare you are holding has expired, the API answers 409 Conflict with a message telling you to search again — as opposed to a 502, which means the supplier itself failed.
- Supplier-specific validation. A payload a supplier rejects on its own rules — most commonly a passenger missing passport details on a Nuitee booking — comes back as 400 Bad Request with the reason in
detail. Fix the payload and resubmit; nothing upstream was touched.
Pricing, markup, currency, the wallet and every response shape are identical across suppliers. An integration written against TravClan works against Nuitee with no changes beyond the three fields above.
Flight Search
/v1/flights/search/
Searches for available flights. Returns up to several hundred options. The exact shape varies slightly for international round-trip — see the dedicated section below.
Common to every search response
Every search response carries two derived fields the partner UI can consume directly:
sort_statsat the top level — pre-computed Cheapest / Fastest / Earliest summaries so the UI doesn't have to scan results client-side. Same shape across all four scenarios; for round-trips thefareandduration_minutesare combined outbound+inbound.kaira_pickedon eachFlightOption— up to three options in each list are markedtrue. An option qualifies only if it (1) has a real, concrete baggage allowance on every segment (we exclude empty values and the string"Reconfirm with airline"— that's TravClan's way of saying "unknown"), (2) is in the top-quartile by price, and (3) is in the top-quartile by total duration. If no option satisfies all three, the cheapest-with-baggage falls through as a single pick. Picks are also moved to the top of their list (ordered cheapest first) so the partner UI sees them before the rest. Applied tooutbound[],inbound[], and each outbound'sinbound_options[](intl round-trip).
"sort_stats": {
"cheapest": { "fare": 5375.00, "duration_minutes": 135, "departure_time": "2026-06-15T22:30:00", "airlines": ["AI"] },
"fastest": { "fare": 5540.00, "duration_minutes": 130, "departure_time": "2026-06-15T06:00:00", "airlines": ["6E"] },
"earliest": { "fare": 5540.00, "duration_minutes": 130, "departure_time": "2026-06-15T06:00:00", "airlines": ["6E"] }
}Two more top-level fields describe the response itself:
provider— which supplier served this search (nuiteeortravclan). Echoed on every option too.round_trip_style—"coupled"or"independent"on round-trip searches,nullotherwise. Read this rather than deriving the layout fromis_domestic: which round trips are coupled is a per-supplier rule. See Suppliers.
Common request body
| Field | Type | Description |
|---|---|---|
| source | string | nuitee (default) or travclan — which supplier serves this search. Case-insensitive. See Suppliers optional |
| direct_flight | boolean | true or false required |
| adult_count | integer | 0 to 9 required |
| child_count | integer | 0 to 9 required |
| infant_count | integer | 0 to 9 required |
| flight_cabin_class | integer | 1=Any cabin · 2=Economy · 3=Premium Economy · 4=Business · 5=Premium Business · 6=First (default 1). Note 1 means any, not economy — send 2 for economy specifically. optional |
| journey_type | integer | 1=One-way · 2=Round-trip · 3=Multi-city required |
| preferred_departure_time | string | ISO format YYYY-MM-DDTHH:mm:ss required |
| preferred_return_departure_time | string | Required for round-trip; same format as above optional |
| origin | string | 3-letter IATA airport code (e.g. "DEL") required |
| destination | string | 3-letter IATA airport code (e.g. "BOM") required |
remaining_seconds. The trace_id returned here is required for every subsequent call in the booking chain (fare-rules, create-booking). TravClan results run a flat 15 minutes. Nuitee results expire with the underlying airline offer, typically 5–10 minutes, and the response tells you exactly when via expires_at and remaining_seconds — honour those rather than assuming 15 minutes. Once expired you must issue a fresh search.The four scenarios
The API handles all four canonical booking scenarios. Pick the tab that matches your route:
Domestic one-way · journey_type=1 · isDomestic=true
The simplest case. Search returns a single list of outbound options.
Request
{
"direct_flight": false,
"adult_count": 1,
"child_count": 0,
"infant_count": 0,
"flight_cabin_class": 1,
"journey_type": 1,
"preferred_departure_time": "2026-06-15T00:00:00",
"origin": "DEL",
"destination": "BOM"
}Response (abridged)
{
"trace_id": "b9754c9f-8bdc-4300-97db-696031eec680",
"currency": "INR",
"journey_type": "one_way",
"is_domestic": true,
"pax_count": { "adult": 1, "child": 0, "infant": 0 },
"outbound": [
{
"result_index": "04726d37-c4d8-40fe-850f-784c5a338ccf",
"is_refundable": true,
"stop_count": 0,
"fare_name": "Published",
"fare_class": "PUBLISHED",
"airline_remark": null,
"group_id": 74,
"kaira_picked": true,
"pricing": {
"currency": "INR",
"base_fare": 4837.50,
"service_fee": 0.0,
"published_fare": 5375.00,
"final_fare": 5375.00
},
"segments": [
{
"airline": { "code": "AI", "name": "Air India", "flight_number": "814", "fare_class": "S" },
"origin": { "airport_code": "DEL", "airport_name": "Indira Gandhi Intl", "city_code": "DEL", "city_name": "Delhi", "country_name": "India", "terminal": "Terminal 3", "departure_time": "2026-06-15T22:30:00" },
"destination": { "airport_code": "BOM", "airport_name": "Chhatrapati Shivaji", "city_code": "BOM", "city_name": "Mumbai", "country_name": "India", "terminal": "Terminal 2", "arrival_time": "2026-06-16T00:45:00" },
"cabin_class": "economy",
"duration_minutes": 135,
"ground_time_minutes": 0,
"baggage_allowance": "25 Kg (01 Piece only)",
"cabin_baggage_allowance": "7 Kg",
"seats_available": 9
}
]
}
],
"inbound": [],
"sort_stats": {
"cheapest": { "fare": 5375.00, "duration_minutes": 135, "departure_time": "2026-06-15T22:30:00", "airlines": ["AI"] },
"fastest": { "fare": 5540.00, "duration_minutes": 130, "departure_time": "2026-06-15T06:00:00", "airlines": ["6E"] },
"earliest": { "fare": 5540.00, "duration_minutes": 130, "departure_time": "2026-06-15T06:00:00", "airlines": ["6E"] }
}
}To book this flight, send result_indices: ["04726d37-c4d8-40fe-850f-784c5a338ccf"] to /v1/flights/booking/.
Domestic round-trip · journey_type=2 · isDomestic=true
Outbound and inbound options are independent lists at the top level. Pick any outbound and any inbound — they pair freely.
Request
{
"direct_flight": false,
"adult_count": 1,
"child_count": 0,
"infant_count": 0,
"flight_cabin_class": 1,
"journey_type": 2,
"preferred_departure_time": "2026-06-15T00:00:00",
"preferred_return_departure_time": "2026-06-20T00:00:00",
"origin": "DEL",
"destination": "BOM"
}Response (abridged)
{
"trace_id": "b9754c9f-8bdc-4300-97db-696031eec680",
"currency": "INR",
"journey_type": "round_trip",
"is_domestic": true,
"pax_count": { "adult": 1, "child": 0, "infant": 0 },
"outbound": [
{
"result_index": "04726d37-c4d8-40fe-850f-784c5a338ccf",
"kaira_picked": true,
"pricing": { "currency": "INR", "final_fare": 5375.00 },
"segments": [ { "origin": { "airport_code": "DEL" }, "destination": { "airport_code": "BOM" }, "...": "..." } ]
}
],
"inbound": [
{
"result_index": "bc3df4cf-4d9e-4e8a-86ba-8e7defca0b4d",
"kaira_picked": true,
"pricing": { "currency": "INR", "final_fare": 5163.18 },
"segments": [ { "origin": { "airport_code": "BOM" }, "destination": { "airport_code": "DEL" }, "...": "..." } ]
}
],
"sort_stats": {
"cheapest": { "fare": 10538.18, "duration_minutes": 260, "departure_time": "2026-06-15T22:30:00", "airlines": ["AI", "6E"] },
"fastest": { "fare": 10703.00, "duration_minutes": 255, "departure_time": "2026-06-15T06:00:00", "airlines": ["6E"] },
"earliest": { "fare": 10703.00, "duration_minutes": 255, "departure_time": "2026-06-15T06:00:00", "airlines": ["6E"] }
}
}/v1/flights/booking/:"result_indices": ["<outbound_rI>", "<inbound_rI>"]International one-way · journey_type=1 · isDomestic=false
Structurally identical to domestic one-way. The only differences are downstream — passenger data must include passport details when you save passengers.
Request
{
"direct_flight": false,
"adult_count": 1,
"child_count": 0,
"infant_count": 0,
"flight_cabin_class": 1,
"journey_type": 1,
"preferred_departure_time": "2026-06-15T00:00:00",
"origin": "DEL",
"destination": "DXB"
}Response (abridged)
{
"trace_id": "d380f8d1-3406-40c8-808a-618ab89f080a",
"currency": "INR",
"journey_type": "one_way",
"is_domestic": false,
"pax_count": { "adult": 1, "child": 0, "infant": 0 },
"outbound": [
{
"result_index": "eb0dd017-c953-408e-b85d-05dbf4f333f3",
"pricing": {
"currency": "INR",
"base_fare": 14818.13,
"service_fee": 0.0,
"published_fare": 17337.60,
"final_fare": 17337.60
},
"segments": [
{
"airline": { "code": "AI", "name": "Air India", "flight_number": "929", "fare_class": "T" },
"origin": { "airport_code": "DEL", "city_name": "Delhi", "country_name": "India", "terminal": "Terminal 3", "departure_time": "2026-06-15T07:00:00" },
"destination": { "airport_code": "DXB", "city_name": "Dubai", "country_name": "United Arab Emirates", "terminal": "Terminal 1", "arrival_time": "2026-06-15T09:30:00" },
"cabin_class": "economy",
"duration_minutes": 240,
"baggage_allowance": "25 KG",
"cabin_baggage_allowance": "7 Kg",
"seats_available": 9
}
]
}
],
"inbound": []
}International round-trip · journey_type=2 · isDomestic=false
Outbound and inbound are coupled. Each outbound option carries its own list of compatible return options under inbound_options. The top-level inbound[] is empty.
- Outbound pricing is "starting from" (cheapest paired inbound).
- Each
inbound_options[j]has the actual combined fare for that outbound + that specific inbound. - To book, pick one
inbound_options[j]and send only itsresult_index— both legs are reconstructed from it.
Request
{
"direct_flight": false,
"adult_count": 1,
"child_count": 0,
"infant_count": 0,
"flight_cabin_class": 1,
"journey_type": 2,
"preferred_departure_time": "2026-06-15T00:00:00",
"preferred_return_departure_time": "2026-06-20T00:00:00",
"origin": "DEL",
"destination": "DXB"
}Response (abridged)
{
"trace_id": "7dbc004b-e2f7-4e51-b6d1-1e15b91b930b",
"currency": "INR",
"journey_type": "round_trip",
"is_domestic": false,
"outbound": [
{
"result_index": "f54e4dff-0efc-4b82-81a5-71d0fc59d6d1",
"pricing": { "currency": "INR", "final_fare": 16428.15 },
"segments": [ { "origin": { "airport_code": "DEL" }, "destination": { "airport_code": "DXB" }, "...": "..." } ],
"inbound_options": [
{
"result_index": "f54e4dff-0efc-4b82-81a5-71d0fc59d6d1",
"pricing": { "currency": "INR", "final_fare": 16428.15 },
"segments": [ { "origin": { "airport_code": "DXB" }, "destination": { "airport_code": "DEL" }, "...": "..." } ]
},
{
"result_index": "57e7968f-64e7-4a22-bae7-ff0391afcbb5",
"pricing": { "currency": "INR", "final_fare": 16428.15 },
"segments": [ { "origin": { "airport_code": "DXB" }, "destination": { "airport_code": "DEL" }, "...": "..." } ]
}
]
}
],
"inbound": []
}result_index is a display-only "starting from" token (copied from the cheapest paired inbound). Do not send it alongside the inbound's result_index. Just send one entry to /v1/flights/booking/:"result_indices": ["<chosen inbound_options[j].result_index>"]Fare Rules
/v1/flights/fare-rules/
Retrieves cancellation and change rules for a specific flight result before locking the booking. Optional but recommended.
Request body
| Field | Type | Description |
|---|---|---|
| trace_id | string | From the search response required |
| result_index | string | The flight option's id required |
Sample request
{
"trace_id": "b9754c9f-8bdc-4300-97db-696031eec680",
"result_index": "04726d37-c4d8-40fe-850f-784c5a338ccf"
}Sample response
{
"trace_id": "b9754c9f-8bdc-4300-97db-696031eec680",
"result_index": "04726d37-c4d8-40fe-850f-784c5a338ccf",
"expires_at": "2026-05-14T10:35:00",
"remaining_seconds": 845,
"rules": [
{
"airline": "AI",
"origin": "DEL",
"destination": "BOM",
"rule_text": "Cancellation charges: INR 3,500 + Airline Fee. Date change: INR 3,000 + fare difference. ..."
}
]
}Create Booking
/v1/flights/booking/
Locks fares with the airline and creates a Neptune Booking record. Returns a stable booking_id used for every subsequent step.
Request body
| Field | Type | Description |
|---|---|---|
| trace_id | string | From the search response required |
| result_indices | array | List of result IDs. See per-scenario rules below. required |
How many result_indices to send
| Scenario | Count | Composition |
|---|---|---|
| Domestic one-way | 1 | The outbound's result_index |
| Domestic round-trip | 2 | [outbound.result_index, inbound.result_index] |
| International one-way | 1 | The outbound's result_index |
| International round-trip | 1 | The chosen inbound_options[j].result_index only |
Sample request (domestic round-trip)
{
"trace_id": "b9754c9f-8bdc-4300-97db-696031eec680",
"result_indices": [
"04726d37-c4d8-40fe-850f-784c5a338ccf",
"bc3df4cf-4d9e-4e8a-86ba-8e7defca0b4d"
]
}Sample response
{
"booking_id": "527668c2-9b07-47f2-9940-0d27a176acd4",
"trace_id": "b9754c9f-8bdc-4300-97db-696031eec680",
"itinerary_code": "itrjieq",
"status": "draft",
"is_domestic": true,
"pax_count": { "adult": 1, "child": 0, "infant": 0 },
"expires_at": "2026-05-14T10:35:00",
"remaining_seconds": 1798,
"pricing": {
"currency": "INR",
"base_fare": 9263.28,
"service_fee": 0.0,
"tax_and_surcharge": 1485.65,
"published_fare": 10748.93,
"final_fare": 10748.93,
"previous_total_amount": 10748.93,
"price_changed": false
},
"pax_rules": {
"leadPax": {
"dateOfBirth": { "isVisible": true, "isMandatoryIfVisible": true },
"contactNumber": { "isVisible": true, "isMandatoryIfVisible": true },
"email": { "isVisible": true, "isMandatoryIfVisible": true },
"passportNumber": { "isVisible": true, "isMandatoryIfVisible": false },
"nationality": { "isVisible": true, "isMandatoryIfVisible": false },
"gstNumber": { "isVisible": true, "isMandatoryIfVisible": false }
}
},
"itinerary_items": [
{
"item_code": "itmqmy2",
"result_index": "04726d37-c4d8-40fe-850f-784c5a338ccf",
"journey_type": "round_trip",
"is_refundable": true,
"fare_name": "Published",
"fare_class": null,
"airline_remark": null,
"legs": [
{ "direction": "outbound", "stop_count": 0, "segments": [ /* ... */ ] }
]
},
{
"item_code": "itmqmyc",
"result_index": "bc3df4cf-4d9e-4e8a-86ba-8e7defca0b4d",
"journey_type": "round_trip",
"is_refundable": true,
"legs": [
{ "direction": "inbound", "stop_count": 0, "segments": [ /* ... */ ] }
]
}
]
}pax_rules before submitting passengers — it tells you which passenger fields are mandatory for this specific booking. International flights, business class, and certain airline-airport pairs each have different rules.expires_at and remaining_seconds from the response to track the window. Submit passengers and book within that time.Save Passengers
/v1/flights/booking/{booking_id}/passengers/
Submits traveller details for the booking. Optionally includes SSR (Special Service Requests — meals, baggage, seats). Validation runs both at the API layer and against the airline's mandatory-field rules (returned in pax_rules from create-booking).
Passenger object
| Field | Type | Description |
|---|---|---|
| title | string | "Mr", "Ms", "Mrs" required |
| first_name | string | required |
| last_name | string | required |
| gender | string | "male", "female", "other" required |
| pax_type | string | "adult", "child", or "infant" required |
| dob | string | Date of birth, YYYY-MM-DD required |
| is_lead | boolean | Exactly one passenger per booking must be lead required |
| string | Lead passenger only — required by paxRules optional | |
| contact_number | string | Lead passenger only — required by paxRules optional |
| cell_country_code | string | e.g. "91" (no leading "+") optional |
| nationality | string | ISO country code (e.g. "IN"); often required for intl optional |
| address_line_one | string | optional |
| address_line_two | string | optional |
| city | string | optional |
| country_code | string | ISO 2-letter (e.g. "IN") optional |
| country_name | string | optional |
| passport_number | string | Required for international bookings optional |
| passport_expiry | string | YYYY-MM-DD; required for international optional |
| passport_issue_date | string | YYYY-MM-DD; often required for international optional |
| frequent_flyer_airline_code | string | If providing FF, send both fields optional |
| frequent_flyer_number | string | optional |
| gst_company_*, gst_number | string | Lead pax only; all-or-none — partial GST is rejected optional |
| ssr | object | Special Service Requests; see the With SSR tab optional |
Plain request — no SSR
The most common case. Just the passenger details.
{
"passengers": [
{
"title": "Mr",
"first_name": "Aman",
"last_name": "Sharma",
"gender": "male",
"pax_type": "adult",
"dob": "1990-01-15",
"is_lead": true,
"nationality": "IN",
"email": "aman@example.com",
"contact_number": "9876543210",
"cell_country_code": "91",
"address_line_one": "12 MG Road",
"address_line_two": "Indiranagar",
"city": "Bangalore",
"country_code": "IN",
"country_name": "India",
"passport_number": "T1234563",
"passport_expiry": "2030-12-06",
"passport_issue_date": "2020-12-06"
}
]
}Request with SSR (meal · baggage · seat)
Add an ssr object to the passenger. Each SSR type is an independent array — include only what you need.
| Field | Type | Description |
|---|---|---|
| ssr.meal[] | array | Meal preferences with cost |
| ssr.baggage[] | array | Extra baggage purchases |
| ssr.seat[] | array | Seat selection with cost |
SSR item fields
| Field | Type | Description |
|---|---|---|
| code | string | SSR code (e.g. "VCSW" for veg meal) required |
| origin | string | 3-letter IATA — the segment this applies to required |
| destination | string | 3-letter IATA — segment destination required |
| amt | integer | Cost in INR optional |
| seat | string | Seat code (e.g. "4B") — for ssr.seat[] entries only optional |
{
"passengers": [
{
"title": "Mr",
"first_name": "Aman",
"last_name": "Sharma",
"gender": "male",
"pax_type": "adult",
"dob": "1990-01-15",
"is_lead": true,
"nationality": "IN",
"email": "aman@example.com",
"contact_number": "9876543210",
"cell_country_code": "91",
"ssr": {
"meal": [
{ "code": "VCSW", "origin": "DEL", "destination": "BOM", "amt": 400 }
],
"baggage": [
{ "code": "IXBB", "origin": "DEL", "destination": "BOM", "amt": 3000 }
],
"seat": [
{ "code": "4B", "origin": "DEL", "destination": "BOM", "amt": 450, "seat": "4B" }
]
}
}
]
}Sample response
{
"booking_id": "527668c2-9b07-47f2-9940-0d27a176acd4",
"trace_id": "b9754c9f-8bdc-4300-97db-696031eec680",
"itinerary_code": "itrjieq",
"status": "passenger_saved",
"is_domestic": true,
"pax_count": { "adult": 1, "child": 0, "infant": 0 },
"pricing": { "...": "..." },
"itinerary_items": [ "..." ],
"duplicates_found": false,
"duplicates": []
}Duplicate booking detection
The system flags potential duplicates (same lead passenger name + same sector + same date) from the last 7 days. When triggered, the response includes:
{
"duplicates_found": true,
"duplicates": [
{
"pnr": "9DYP4N",
"status": "CONFIRMED",
"bms_booking_code": "tk34z",
"member_code": "mjcoq",
"origin_city_code": "DEL",
"destination_city_code": "BOM",
"created_at": "2026-05-12T18:21:32.000Z"
}
]
}The booking is not blocked — your application decides whether to proceed to /book/ or surface a warning to the user.
Reprice
/v1/flights/booking/{booking_id}/reprice/
Revalidates the price with the airline before committing. If the fare has moved since create-booking, pricing.price_changed is true and your application should confirm with the user before booking.
Request
No request body required — the booking_id is in the path.
curl -X POST https://dev.neptune.tarzanway.com/v1/flights/booking/527668c2-.../reprice/ \
-H "Authorization: Api-Key YOUR_KEY"Sample response
{
"booking_id": "527668c2-9b07-47f2-9940-0d27a176acd4",
"status": "priced",
"pricing": {
"currency": "INR",
"base_fare": 9263.28,
"tax_and_surcharge": 1485.65,
"published_fare": 10748.93,
"final_fare": 10748.93,
"previous_total_amount": 10748.93,
"price_changed": false
},
"itinerary_items": [ "..." ]
}Book
/v1/flights/booking/{booking_id}/book/
Issues the PNR. Synchronous — the response carries the confirmation. Once this returns 200, the booking is committed with the airline.
Request
curl -X POST https://dev.neptune.tarzanway.com/v1/flights/booking/527668c2-.../book/ \
-H "Authorization: Api-Key YOUR_KEY"Sample response
{
"booking_id": "527668c2-9b07-47f2-9940-0d27a176acd4",
"status": "confirmed",
"pnr": "ABC123",
"bms_booking_code": "tk34z",
"itinerary_code": "itrjieq",
"is_domestic": true,
"pax_count": { "adult": 1, "child": 0, "infant": 0 },
"pricing": { "...": "..." },
"itinerary_items": [ "..." ]
}book fails upstream, the booking moves to status failed — not left in limbo. Inspect GET /v1/flights/booking/{id}/ for context, or retry with a fresh create-booking.Get Booking
/v1/flights/booking/{booking_id}/
Returns the current state of a booking — including the latest pricing and PNR if confirmed.
Sample response
{
"booking_id": "527668c2-9b07-47f2-9940-0d27a176acd4",
"trace_id": "b9754c9f-...",
"itinerary_code": "itrjieq",
"status": "confirmed",
"pnr": "ABC123",
"is_domestic": true,
"pricing": { "...": "..." },
"itinerary_items": [ "..." ]
}List Bookings
/v1/bookings/?limit=25&offset=0
Paginated list of your own bookings. Most recent first.
Query parameters
| Param | Type | Description |
|---|---|---|
| limit | integer | Page size, max 100 (default 25) |
| offset | integer | Skip N items |
Sample response
{
"count": 132,
"next": "https://dev.neptune.tarzanway.com/v1/bookings/?limit=25&offset=25",
"previous": null,
"results": [
{
"id": "527668c2-9b07-47f2-9940-0d27a176acd4",
"status": "confirmed",
"pnr": "ABC123",
"trace_id": "b9754c9f-...",
"itinerary_code": "itrjieq",
"currency": "INR",
"amount_charged": 10748.93,
"wallet_debited_inr": 10748.93,
"passengers": [ "..." ],
"flight_segments": [ "..." ],
"created_at": "2026-05-14T10:12:33Z",
"modified_at": "2026-05-14T10:15:01Z"
}
]
}Booking Lifecycle
Every booking moves through a deterministic set of statuses. Each transition is recorded on the booking's event log for audit.
| Status | Set by | Meaning |
|---|---|---|
| draft | create-booking | Fares locked with the airline; no passengers yet. |
| passenger_saved | save-passengers | Travellers submitted; ready to reprice or book. |
| priced | reprice | Latest fare quote from the airline. Inspect pricing.price_changed. |
| confirmed | book | PNR issued. Terminal success state. |
| failed | book (on upstream error) | Upstream rejected the booking. Inspect the response for details. |
| cancelled | (cancellation flow — coming soon) | Booking cancelled with the airline. |
Universal Response Shape
Every successful response shares a small set of conventions. Knowing them once means you don't have to learn them per endpoint.
Pricing object
Found on every flight option, itinerary, and booking response. final_fare is the amount you'll be charged for the booking.
{
"currency": "INR",
"base_fare": 4837.50,
"service_fee": 0.0,
"published_fare": 5375.00,
"final_fare": 5375.00
}Itinerary-level responses (create-booking, reprice, get-booking) also include tax_and_surcharge, previous_total_amount, and price_changed (which goes true when the fare moves during a reprice). Search options additionally carry an airline_remark, fare_name, and group_id at the option level.
Error envelope
All non-2xx responses share the same shape:
{
"success": false,
"errors": [
{ "detail": ["Authentication credentials were not provided."] }
]
}Airports Autocomplete
GET
/v1/airports/autocomplete/?q=<query>
Public typeahead used by the portal's origin / destination pickers. Backed by mercury's curated hub table — same dataset the consumer site uses. The endpoint is unauthenticated (no API key needed) since it's purely for UI autocomplete; results are cached for an hour.
Query parameters
| Param | Type | Notes |
|---|---|---|
q | string (min length 2) | Matches against airport name, city name, or IATA code (prefix-match). |
Response
{
"results": [
{ "code": "DEL", "city": "New Delhi", "name": "Delhi" },
{ "code": "DXB", "city": "Dubai", "name": "Dubai Intl" }
]
}Empty results for queries under 2 characters or with no matches. 502 if mercury is unreachable.
Error Handling
| Status | Meaning | Common causes |
|---|---|---|
| 400 | Bad Request | Invalid payload — missing fields, invalid date format, bad cabin class, or other request validation failures. |
| 401 | Unauthorized | Missing, malformed, or revoked API key. |
| 404 | Not Found | Booking ID doesn't exist or doesn't belong to your partner account. |
| 429 | Too Many Requests | You've exceeded your rate limit. Back off and retry with exponential delay. |
| 502 | Bad Gateway | Upstream provider error. Often transient — retry once. If persistent, the upstream message is in errors[0].detail. |
| 500 | Internal Server Error | Bug on our side. Save the X-Request-Id response header and reach out. |
X-Request-Id header. Include it when reporting issues — it lets us trace your request through every layer in under a minute.Rate Limits
| Endpoint | Per-partner limit |
|---|---|
/v1/flights/search/ | 60 requests/min |
/v1/flights/booking/{id}/book/ | 30 requests/min |
| All others | 120 requests/min (combined) |
Higher limits are available on request as part of partner onboarding. Long-term sustained load is typically bursty — short spikes well above these are acceptable; sustained breach triggers 429.
API Status
Run the four end-to-end smoke tests against this environment on demand. Each test exercises the full booking chain (search → fare-rules → create-booking → save-passengers → reprice) without issuing a real PNR.
- Domestic one-way —
- Domestic round-trip —
- International one-way —
- International round-trip —
Each check takes 20–60s. Booking is skipped (--skip-book) so no real PNR is issued and no wallet balance is debited.
FAQ
How do I get an API key?
API keys are issued by Neptune after a partnership agreement is signed. Once onboarded, you receive a unique key per environment (sandbox + production). Reach out to your account contact to start the process.
How is pricing determined?
Pricing terms are agreed during onboarding and are baked into the rates you see in every response. The final_fare on each search result and on the booking is exactly what you'll be charged — no hidden line items, no end-of-month adjustments. Differentiated pricing (e.g. by route or cabin class) is something we can discuss on the commercial side.
How long is a search result valid?
Each search response is valid for exactly 15 minutes. The trace_id from your search response is required for every subsequent call in the booking chain (fare-rules, create-booking). After 15 minutes the trace_id expires and you must issue a fresh search. Once you've called create-booking, the locked fares remain valid typically for another 15–30 minutes (track expires_at / remaining_seconds).
What happens if a duplicate booking is detected?
The save-passengers response includes duplicates_found: true and a list of matching recent bookings. Neptune does not block the booking — your application chooses whether to proceed to /book/ or warn the user first. Duplicate detection is based on lead passenger name + sector + date within the last 7 days.
Can I cancel a confirmed booking via the API?
Cancellation is not exposed yet via the v1 API. To cancel, contact support with the booking_id and PNR. A native cancel endpoint is planned for v1.1.
Can I see fare rules before locking a booking?
Yes — call POST /v1/flights/fare-rules/ with the trace_id and the result_index. This is a read-only check; it doesn't consume any inventory and is free of charge.
Are passport details mandatory for international flights?
Generally yes. The exact requirements are returned by create-booking under pax_rules.leadPax and pax_rules.adult. Look for fields where isVisible: true and isMandatoryIfVisible: true. For most international airlines, passportNumber, passportExpiry, and nationality are required.
How do I add a meal, seat, or extra baggage to a booking?
Include an ssr object in each relevant passenger when calling save-passengers. Meal, baggage, and seat are independent — include only what's needed. See the With SSR tab under Save Passengers.
Is there a sandbox?
Yes. Sandbox is provisioned alongside your production credentials. Bookings issued in sandbox are not real PNRs and won't be ticketed. Use sandbox to validate your integration end-to-end; production is enabled after sign-off.
How does billing work?
Neptune runs on a prepaid wallet model. You recharge your account balance with us in advance, and each successful /book/ deducts the booking total from that balance. There's no per-transaction invoice or end-of-month settlement — you always see your live balance, and bookings fail with a clear error if you don't have enough credit. Top-up requests, balance enquiries, and statements go through your account contact for now; a self-serve recharge endpoint is on the roadmap.
Payment collection from your end customer still happens on your platform — Neptune isn't a payment processor. You charge the customer, then call /book/ against your prepaid balance.
Is the booking_id stable for refunds and disputes?
Yes — booking_id is a stable UUID issued by Neptune. It survives PNR cancellation, refund flow, and any state transition. Always store this on your side for cross-referencing.
What if an upstream provider is down?
You'll receive 502 Bad Gateway with a descriptive error message. These are typically transient (under 30 seconds). Retry with backoff. If persistent, check our status page or reach out to your account contact.