Originally published at sunnybadgujar.com. Amadeus is where most travel portals start, and for good reason: it's the largest GDS, the content is deep, and the modern Amadeus for Developers APIs are plain REST with JSON — no arcane messaging formats to learn on day one. But there's a gap between "I can search flights from a code sample" and "I've shipped a B2B travel portal that sub-agents actually book on." This post is about closing that gap. I run a travel reservation platform that integrates Amadeus alongside other suppliers end to end — search, pricing, booking, refunds, agent wallets and markup. Below is the integration path I'd hand to someone starting today, in the order the decisions actually come up. First decision: Self-Service or Enterprise? Before you write a line of code, you have to know which Amadeus you're building against, because they are not the same product. Amadeus Self-Service APIs. Sign up on the developer portal, get an API key in minutes, and you're calling a free test environment the same day. REST/JSON, pay-as-you-go pricing in production, no commercial negotiation. Perfect for MVPs, prototypes, and portals with moderate volume. The catch: the content and functionality are a curated subset of full GDS capability. Amadeus Enterprise (the full travel platform). Full GDS content, ticketing, richer fare and ancillary support — but it requires a commercial agreement, an office ID, and a heavier onboarding. This is what large agencies and consolidators run on. Practical advice: build your MVP on Self-Service. Its data model and flow mirror the enterprise concepts closely enough that if you keep Amadeus behind an adapter (more on that below), moving up later is an integration change, not a rewrite. Don't block your launch on an enterprise contract you don't need yet. Everything that follows uses the Self-Service REST flow, because that's where 90% of new B2B portals begin. Authentication: one token, cached, refreshed Amadeus uses OAuth2 client credentials. You exchange your API key and secret for an access token, and you send that token as a bearer header on every subsequent call. The token is short-lived — roughly 30 minutes — so you cache it and refresh before it expires rather than requesting a new one per call. POST https://test.api.amadeus.com/v1/security/oauth2/token Content-Type: application/x-www-form-urlencoded grant_type=client_credentials &client_id=YOUR_API_KEY &client_secret=YOUR_API_SECRET // response { "access_token": "abc123...", "expires_in": 1799, "token_type": "Bearer" } Wrap this in a small token provider that stores the token in memory with its expiry and hands out a valid one on demand. Requesting a fresh token on every search is a classic early mistake — it adds a round-trip to every request and burns quota for no reason. Two environments, two base URLs: test.api.amadeus.com serves cached, non-live data for development; api.amadeus.com is production with live pricing and real bookings. They use different credentials. Make the base URL and keys configuration, never hard-coded — you will switch between them constantly. The core flight flow is three calls, in order This is the single most important thing to internalise about Amadeus, and it maps directly to how travel actually works: search is indicative, price is live, and only then can you book. Three endpoints, always in this sequence: Flight Offers Search — POST /v2/shopping/flight-offers. Send origin, destination, dates, passenger counts, cabin. Get back a list of offers, each with a full fare breakdown and an offer object you carry forward. This is what populates your results page. Flight Offers Price — POST /v1/shopping/flight-offers/pricing. Take the exact offer the customer selected and confirm it live. Amadeus revalidates availability and price and returns the current, bookable fare. This is where you catch price drift before it costs you money. Flight Create Orders — POST /v1/booking/flight-orders. Send the confirmed offer plus traveller details, and Amadeus creates the booking (a PNR). This is the step that actually reserves seats. Skipping the middle step is the most expensive bug in the category. If you book straight off the search result, you'll routinely try to sell fares that have already changed or sold out — and you eat the difference or fail the booking at the worst possible moment. The price call exists precisely so you re-confirm the fare the instant before you charge the customer. Booking is two steps for the same reason. Confirm price live, then take payment, then create the order. If order creation fails after you've charged, you must automatically void or refund — never leave an agent's wallet debited for a PNR that doesn't exist. Now the part the docs don't cover: this is a B2B portal A consumer booking site has one customer type. A B2B portal has agents — sub-agents, sub-sub-agents, corporate clients — each of whom logs in, sees their own fares, and books against their own money. That changes the architecture in four concrete ways. 1. Multi-tenancy and roles Your data model needs a tenant hierarchy from day one: the portal owner at the top, then agencies, then the individual agents who log in. Every booking, every wallet transaction, every markup rule is scoped to a node in that tree. Retrofitting multi-tenancy after launch is painful — bake the agency/agent relationship into your schema and every query before you have live data. 2. The markup engine Amadeus returns net fares. Your agents must never see the net — they see the price after your markup. So between the pricing call and the response you send to the browser, you run a markup engine that adds a margin. And it's rarely a flat number: markup varies by agency, by route, by airline, by fare class, sometimes as a percentage and sometimes as a fixed amount per passenger. // conceptual: applied server-side, never in the client sellFare = netFare + resolveMarkup(agencyId, route, airline, fareClass); // the agent sees sellFare; the net stays server-side only Two rules keep this safe. First, markup is applied on the server, always — the net fare must never reach the browser, or an agent will read it in the network tab. Second, you store both the net and the sell price on the booking record, so your reconciliation and commission reports are accurate later. 3. Agent wallets and credit B2B agents don't pay by card per booking. They top up a wallet (or get a credit line), and each booking debits the balance. That means your booking flow has an extra gate: check the agent has sufficient balance before you call Flight Create Orders, then debit atomically as part of confirming the booking. Model the wallet as an immutable ledger — append credits and debits, never edit a running total. The balance is the sum of entries. This makes disputes auditable and prevents a whole class of race-condition bugs. Debit and booking must succeed or fail together. If the Amadeus order fails, the debit reverses; if the debit fails (insufficient funds), the order is never attempted. Cancellations and refunds credit the wallet back, minus any airline penalty — as a background job with retries, because refund calls fail transiently. 4. Credential and quota management On Self-Service, the whole portal typically shares one set of production Amadeus credentials, so your rate limiting and quota tracking are portal-wide — one noisy agency hammering search can throttle everyone. Enforce per-agency rate limits inside your own layer so no single tenant can exhaust the shared quota. (On enterprise, you may have per-office-ID credentials, which changes this calculus — another reason to keep Amadeus behind an adapter.) Keep Amadeus behind an adapter — even if it's your only supplier It's tempting, when Amadeus is your only integration, to call it directly from your booking code. Don't. Define your own internal types — a FlightOffer, a PriceBreakdown, a BookingResult — and make an Amadeus adapter translate into them. Your search page, booking flow and admin panel work only with your model and never import Amadeus specifics. You get two payoffs. Moving from Self-Service to enterprise later becomes an adapter change instead of a rewrite. And the day you add a second supplier — a low-cost carrier aggregator, a direct hotel feed, another GDS — you write one more adapter and the rest of the system doesn't notice. That multi-supplier architecture is a topic in itself; I've written it up separately in how to integrate multiple GDS providers into one booking engine. Caching, rate limits and the test-data trap GDS calls cost money and are rate-limited, so treat them accordingly: Cache searches briefly. A 30–120 second cache keyed on route and dates absorbs pagination and repeat searches without re-hitting Amadeus. Never cache long enough to serve a stale fare into a booking — the live price call is your safety net, but don't lean on it to paper over reckless caching. Rate-limit per agency, inside your layer. As above, the shared Self-Service quota means one tenant can starve the rest unless you police it yourself. Remember the test environment is not live. Test data is cached and limited — some routes return nothing, prices aren't real, and a "successful" test booking isn't a real reservation. Validate business logic in test, but only trust behaviour you've re-checked against production before you go live. The admin panel is half the product None of the above is visible to your operations team unless you build for them. An internal admin panel — showing every booking with its agency, net and sell fare, wallet transaction, PNR and refund state, plus a full audit log of what you sent Amadeus and what came back — is how support resolves a stuck booking at midnight without reading raw API logs. In a B2B portal it's not optional: it's where you manage agents, set markup rules, top up wallets and investigate disputes. Build it alongside the engine, not after. Pitfalls I'd flag before you start Never skip Flight Offers Price. Search prices are indicative; confirm live before every charge. Never let the net fare reach the browser. Apply markup server-side; store both net and sell on the booking. Make wallet and booking atomic. A retried booking or refund must never double-charge or double-credit an agent. Don't hard-code the environment. Test and production have different URLs and keys — make them configuration. Don't call Amadeus directly from business code. The adapter is what lets you grow from Self-Service to enterprise, and from one supplier to many. Log every request and response for audit. "What exactly did we send and what did they say" is a question you'll be asked constantly. Get the three-step flow and the B2B plumbing — markup, wallets, multi-tenancy — right, and Amadeus becomes a dependable engine you can build a real agency business on. Rush them, and you'll be reconciling mismatched fares and disputed wallet balances by hand for months. The API is the easy part; the portal around it is the product. I've integrated Amadeus and other GDS suppliers into a live travel platform — search, pricing, booking, markup, agent wallets and refunds. If you're building a B2B travel portal, you can read more about my travel portal development work or get in touch.