Travel API integration

B2C Travel Booking Engine Development Guide

Jul 13, 2026Hardeep SinghTravel API integration

B2C Travel Booking Engine Development Guide

B2C Travel Booking Engine Development Guide

A B2C travel booking engine is the system of record that sits between your customer's browser and dozens of third-party suppliers — GDS/NDC flight sources, hotel aggregators, transfer and activity providers, and payment gateways. Unlike a simple booking form, a production-grade engine has to normalize inconsistent supplier data, cache aggressively without serving stale prices, fail gracefully when a supplier times out, and settle payments in a PCI-compliant way — all within the two-to-four second window travelers expect from a modern search result.

This guide breaks down the architecture, integration patterns, and operational concerns that go into building a booking engine that can actually handle production traffic, not just a demo.

What a B2C Travel Booking Engine Actually Does

At its core, a booking engine performs four jobs:

  • Search aggregation — fan out a single user query to multiple suppliers, normalize the results into one schema, and rank them
  • Availability and pricing — confirm live rates and seat/room availability before checkout, since search results are rarely guaranteed
  • Booking orchestration — reserve, confirm, and store the booking across one or more supplier systems, often with multi-step rollback logic
  • Post-booking lifecycle — manage confirmations, cancellations, refunds, and customer support tickets tied to a PNR or booking reference

Each of these stages talks to different supplier APIs with different auth schemes, response formats, and failure behaviors, which is why the integration layer — not the UI — is where most of the engineering effort goes.

High-Level Architecture

flowchart TB A[Customer Web / Mobile App] --> B[API Gateway] B --> C[Search Orchestration Service] B --> D[Booking Service] B --> E[Payment Service] C --> F[Supplier Adapter Layer] D --> F F --> G1[Flight APIs - GDS/NDC] F --> G2[Hotel APIs] F --> G3[Transfer/Activity APIs] C --> H[(Redis Cache)] D --> I[(Booking Database)] E --> J[Payment Gateway / PCI Vault] F --> K[Circuit Breaker Layer]

The API Gateway handles authentication, rate limiting, and request routing. Behind it, search and booking are deliberately separated into different services, since search traffic is read-heavy and cacheable, while booking traffic is write-heavy and needs strict consistency.

The Supplier Adapter Layer: Adapter and Factory Patterns

Every supplier returns data in its own shape. A GDS flight response looks nothing like an NDC response, and a hotel aggregator's rate plan structure looks nothing like a direct-connect hotel API. Rather than let this variation leak into your business logic, the adapter pattern normalizes each supplier behind a common interface.

// Common interface every supplier adapter implements
class SupplierAdapter {
  async search(query) { throw new Error("Not implemented"); }
  async getAvailability(offerId) { throw new Error("Not implemented"); }
  async book(offerId, passengerDetails) { throw new Error("Not implemented"); }
}

class AmadeusFlightAdapter extends SupplierAdapter {
  async search(query) {
    const raw = await amadeusClient.shopping.flightOffersSearch.get(query);
    return raw.data.map(this.normalizeOffer);
  }
  normalizeOffer(offer) {
    return {
      supplier: "amadeus",
      price: offer.price.total,
      currency: offer.price.currency,
      segments: offer.itineraries,
    };
  }
}

class HotelbedsAdapter extends SupplierAdapter {
  async search(query) {
    const raw = await hotelbedsClient.hotels.search(query);
    return raw.hotels.map(this.normalizeHotel);
  }
}

A factory then decides which adapter to instantiate based on supplier configuration, product type, or region — so the orchestration layer never needs to know which supplier it's actually talking to:

class SupplierAdapterFactory {
  static create(supplierType) {
    switch (supplierType) {
      case "amadeus": return new AmadeusFlightAdapter();
      case "hotelbeds": return new HotelbedsAdapter();
      case "sabre-ndc": return new SabreNDCAdapter();
      default: throw new Error(`Unknown supplier: ${supplierType}`);
    }
  }
}

This pattern is what lets you add a new supplier — say, swapping in a new NDC-certified airline — without touching the search orchestration or booking logic at all.

Handling Supplier Failures: The Circuit Breaker Pattern

Supplier APIs go down, rate-limit you, or respond slowly during peak load. Without protection, a single slow supplier can cascade into a full search-page timeout. A circuit breaker tracks failure rates per supplier and stops sending traffic to a struggling supplier before it degrades the whole search response.

stateDiagram-v2 [*] --> Closed Closed --> Open: failure threshold exceeded Open --> HalfOpen: timeout period elapses HalfOpen --> Closed: test request succeeds HalfOpen --> Open: test request fails Closed --> Closed: request succeeds
class CircuitBreaker {
  constructor(threshold = 5, resetTimeoutMs = 30000) {
    this.failureCount = 0;
    this.threshold = threshold;
    this.resetTimeoutMs = resetTimeoutMs;
    this.state = "CLOSED";
    this.lastFailureTime = null;
  }

  async call(fn) {
    if (this.state === "OPEN") {
      const cooldownElapsed = Date.now() - this.lastFailureTime > this.resetTimeoutMs;
      if (!cooldownElapsed) throw new Error("Circuit open — supplier temporarily skipped");
      this.state = "HALF_OPEN";
    }
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (err) {
      this.onFailure();
      throw err;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    this.state = "CLOSED";
  }

  onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    if (this.failureCount >= this.threshold) this.state = "OPEN";
  }
}

In practice, each supplier adapter is wrapped with its own circuit breaker instance, so a Hotelbeds outage doesn't affect your Amadeus or transfer-API calls at all — search simply returns results from the suppliers that are healthy.

Caching Strategy with Redis

Search is the highest-traffic, most repeatable part of a booking engine — the same route and date combination gets queried constantly. Caching search results (with short TTLs) dramatically reduces supplier API costs and improves response times, but pricing data ages quickly, so cache invalidation has to be deliberate.

async function searchWithCache(query) {
  const cacheKey = `search:${query.origin}:${query.destination}:${query.date}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const results = await orchestrateSupplierSearch(query);
  // Short TTL — flight/hotel prices change frequently
  await redis.set(cacheKey, JSON.stringify(results), "EX", 90);
  return results;
}

A common pattern is a two-tier cache: a short-lived (60–120 second) cache for raw search results, and a longer-lived cache (hours) for relatively static data like airport metadata, hotel content, or supplier configuration — which doesn't need to be refetched on every request.

Search-to-Booking Sequence

sequenceDiagram participant U as User participant G as API Gateway participant S as Search Service participant C as Redis Cache participant F as Supplier Adapters participant B as Booking Service participant P as Payment Gateway U->>G: Search flights/hotels G->>S: Route search request S->>C: Check cache alt Cache hit C-->>S: Return cached results else Cache miss S->>F: Fan-out to suppliers F-->>S: Normalized offers S->>C: Store with short TTL end S-->>U: Ranked results U->>G: Select offer + confirm booking G->>B: Create booking B->>F: Verify live availability F-->>B: Confirmed B->>P: Process payment P-->>B: Payment success B-->>U: Booking confirmation

Note the re-verification step before payment: since search results may be several minutes old (or served from cache), the booking service always re-checks live availability and price with the supplier before charging the customer. Skipping this step is one of the most common causes of "price changed after booking" complaints.

Payment Processing and PCI DSS Compliance

Payment handling deserves its own dedicated service, isolated from search and booking logic, for one main reason: reducing PCI DSS scope. Rather than handling raw card data yourself, most B2C engines tokenize card details through the payment gateway (Stripe, Razorpay, or a card-present-compliant PSP) and store only the token.

Key practices:

  • Never log or persist raw card numbers, CVVs, or full card data anywhere in your stack
  • Use gateway-hosted fields or tokenization SDKs so card data never touches your servers directly
  • Support multi-currency settlement if you sell to international travelers
  • Reconcile payment webhooks against booking status asynchronously, since gateway confirmation and supplier confirmation don't always arrive in the same order

Common Integration Challenges

Inconsistent supplier response times. Flight search fan-outs are only as fast as the slowest supplier you wait for. Setting per-supplier timeouts (and returning partial results once a reasonable threshold passes) keeps search responsive even when one supplier lags.

Rate limiting. Most GDS and hotel APIs enforce strict per-second or per-minute call limits. A request queue with backoff, combined with the caching layer above, keeps you within limits during traffic spikes.

Duplicate and near-duplicate offers. Aggregating multiple suppliers often surfaces the same flight or room from different sources at different prices. De-duplication logic based on normalized identifiers (flight number + date + cabin, or hotel ID + room type) is necessary before displaying results.

Booking rollback. A multi-supplier itinerary (flight + hotel + transfer) can partially succeed if one leg fails after another has already been confirmed. Booking orchestration needs explicit compensation logic — cancelling already-confirmed legs — rather than assuming all-or-nothing success.

Scalability Considerations

As booking volume grows, a few architectural decisions matter more than raw server capacity:

  • Stateless services behind a load balancer, with session and cart state stored in Redis rather than in-memory
  • Async processing for non-blocking tasks like confirmation emails, invoice generation, and loyalty point updates, handled via a message queue rather than inline with the booking request
  • Read replicas for the booking database, since customer dashboards and reporting queries shouldn't compete with live booking writes
  • Horizontal scaling of the supplier adapter layer independently from the booking service, since search traffic and booking traffic scale differently

Conclusion

A B2C travel booking engine is ultimately an integration and reliability problem as much as it is a UI problem. Getting the supplier adapter layer right — with adapters and factories for normalization, circuit breakers for resilience, and a well-tuned Redis caching strategy — determines whether your platform holds up under real booking volume or falls over the first time a supplier has a bad day.

Agencies building or scaling a booking engine benefit from starting with this architecture rather than retrofitting it later, since resilience and caching patterns are far harder to bolt onto an existing monolith than to design in from the start.

Similar Articles