Hermeseus

Streaming flight search

Stream flight results as they arrive over server-sent events, for a fast first paint and progressively filling results.

POST/Air/AirLowFareSearchIncremental

Access requires the permission air_low_fare_search

This is the streaming twin of AirLowFareSearch. It takes the same request body. Instead of one JSON response, it returns a text/event-stream. Read it as a stream and keep the connection open until the done event.

Request

The body is identical to AirLowFareSearch.

curl -N -X POST https://api.hermeseus.com/api/Air/AirLowFareSearchIncremental \
  -H "Content-Type: application/json" \
  -d '{
    "SessionId": "8f3c81d2-4a5b-4c6d-9e0f-1a2b3c4d5e6f",
    "AdultCount": 1,
    "PricingSourceType": "All",
    "RequestOption": "All",
    "TravelPreference": { "CabinType": 3 },
    "OriginDestinationInformations": [
      { "OriginLocationCode": "IST", "DestinationLocationCode": "DXB", "DepartureDateTime": "2026-10-12T00:00:00" }
    ]
  }'
const res = await fetch("https://api.hermeseus.com/api/Air/AirLowFareSearchIncremental", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    SessionId: "8f3c81d2-4a5b-4c6d-9e0f-1a2b3c4d5e6f",
    AdultCount: 1,
    PricingSourceType: "All",
    RequestOption: "All",
    TravelPreference: { CabinType: 3 },
    OriginDestinationInformations: [
      { OriginLocationCode: "IST", DestinationLocationCode: "DXB", DepartureDateTime: "2026-10-12T00:00:00" }
    ]
  })
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let itineraries = [];

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  for (const block of decoder.decode(value).split("\n\n")) {
    if (!block.startsWith("event:")) continue;
    const event = block.match(/event: (.*)/)[1];
    const data = JSON.parse(block.match(/data: (.*)/)[1]);
    if (event === "itineraries") itineraries = data.PricedItineraries; // replace, do not append
    if (event === "done") console.log("finished", data.Total);
  }
}
import json
import requests

payload = {
    "SessionId": "8f3c81d2-4a5b-4c6d-9e0f-1a2b3c4d5e6f",
    "AdultCount": 1,
    "PricingSourceType": "All",
    "RequestOption": "All",
    "TravelPreference": {"CabinType": 3},
    "OriginDestinationInformations": [
        {"OriginLocationCode": "IST", "DestinationLocationCode": "DXB", "DepartureDateTime": "2026-10-12T00:00:00"}
    ],
}

with requests.post(
    "https://api.hermeseus.com/api/Air/AirLowFareSearchIncremental",
    json=payload, stream=True,
) as res:
    event = None
    for line in res.iter_lines(decode_unicode=True):
        if line.startswith("event:"):
            event = line.split(": ", 1)[1]
        elif line.startswith("data:"):
            data = json.loads(line.split(": ", 1)[1])
            if event == "itineraries":
                itineraries = data["PricedItineraries"]  # replace, do not append

Response

The response is a stream of events. Each event has an event line and a data line.

EventDataMeaning
itineraries{ "PricedItineraries": [ ... ], "Replace": true }The complete current basket. Replace your list with it. Do not append.
done{ "Success": true, "SearchId": N, "Total": N }Terminal. The search finished. SearchId works like the blocking endpoint.
error{ "Success": false, "Error": { "Id", "Message" } }Terminal. The search failed.
event: itineraries
data: {"PricedItineraries":[ { "FareSourceCode": "...", "AirItineraryPricingInfo": { "ItinTotalFare": { "TotalFare": 219.60, "Currency": "USD" } } } ],"Replace":true}

event: itineraries
data: {"PricedItineraries":[ { "FareSourceCode": "..." }, { "FareSourceCode": "..." } ],"Replace":true}

event: done
data: {"Success":true,"SearchId":428713,"Total":137}
Replace, do not append. Every itineraries event carries the whole basket so far, sorted and de-duplicated. Overwrite your list each time rather than adding to it.