How to Build an AI Travel Agent That Plans Real Trips

Denis Gramm
By Denis Gramm · · 12 min read

I hope you enjoy reading this blog post. If you want our team to help you with hotel data integration, click here.

Quick summary

A build-along tutorial: a small web app where a tool-calling LLM plans trips with live hotel prices and ratings. Real code, real screenshots, about 200 lines total.

Whiteboard diagram of a trip request flowing through an AI agent that checks live hotel data before producing an itinerary (StayAPI ai travel agent tutorial)

This tutorial builds an AI travel agent from scratch: a small web app where you type "3 nights in Lisbon in October, mid-range budget" and get back a hotel shortlist with tonight's actual prices, plus a day-by-day plan.
The trick that separates an agent from a chatbot: before answering, the model calls a live hotel data API, so every hotel it recommends is real, bookable, and priced.
Two files, about 200 lines, no framework, no database.
We built it while writing this, and every screenshot below comes from that build.

Key Takeaways

  • An AI travel agent is an LLM plus a data tool in a loop. The model decides when to search; your server executes the search and feeds results back.
  • Ungrounded models invent hotels. Wiring one tool (live hotel search) into the loop is what makes the output trustworthy.
  • The whole app is 2 files and about 200 lines. Node 20+, Express, one LLM API, one hotel data API.
  • Expect one round of prompt fixes. Our first run asked clarifying questions instead of searching; a two-line system-prompt change fixed it. That's normal.
  • Each plan takes 20-40 seconds and one hotel-data request. Agent loops trade speed for grounding; design the UI around the wait.

What an AI Travel Agent Actually Is

If you ask a bare LLM to plan a trip, it writes plausible fiction: hotel names that sound right, prices from its training data, availability it can't know.
The fix is the same pattern behind every serious answer to "how to build an AI agent": give the model tools.
A tool is a function you describe to the model (name, parameters, what it returns).
The model doesn't execute anything; it replies with "call search_hotels with these arguments," your server runs the real request, returns the result, and the model continues with facts in hand.
That loop (model → tool call → real data → model → answer) is the entire architecture.
We built a review feed the same way in our Claude Code tutorial, but there the AI wrote the code; here the AI is inside the product, making runtime decisions.

What You're Building

Finished trip agent app showing a Lisbon hotel shortlist with live prices and ratings above a day-by-day plan (StayAPI ai travel agent tutorial)

One page, one input.
The browser posts your trip request to a local Express server; the server runs the agent loop with a hotel-search tool; the response renders as hotel cards (name, nightly price, guest rating, review count) and day cards.

Step What you build What you see
1 The data proxy Raw JSON of real Lisbon hotels in your browser
2 The agent loop A plain-text trip plan grounded in that data
3 Structured output + UI The styled shortlist and day cards above

Before You Start

  1. Node.js 20.6 or newer (built-in fetch and --env-file).
  2. An OpenAI API key (we use gpt-5-mini; any tool-calling model works, and the loop is 10 lines to swap).
  3. A StayAPI key: sign up at stayapi.com. Free tier, no credit card.
  4. A folder with npm init -y, npm install express, "type": "module" added to package.json, and a .env file holding OPENAI_API_KEY and STAYAPI_API_KEY. The key stays in .env and never reaches the browser.

Step 1: Prove the Data

Agents are only as good as their tools, so start by making the tool work alone.
Create server.js with a search function and one debug route:

import express from "express";

const app = express();
app.use(express.json());
app.set("json spaces", 2);
app.use(express.static("public"));

async function searchHotels(args) {
  const params = new URLSearchParams({
    query: args.query, check_in: args.check_in, check_out: args.check_out,
    adults: "2", currency: "USD"
  });
  if (args.min_rating) params.set("min_rating", String(args.min_rating));
  const res = await fetch(`https://api.stayapi.com/v1/google_travel/search?${params}`, {
    headers: { "x-api-key": process.env.STAYAPI_API_KEY }
  });
  if (!res.ok) throw new Error(`StayAPI returned ${res.status}: ${await res.text()}`);
  const data = await res.json();
  return data.results.slice(0, 8).map(h => ({
    name: h.name, rating: h.rating, review_count: h.review_count,
    stars: h.hotel_class?.stars ?? null, price_per_night: h.price?.formatted ?? null,
    description: h.description
  }));
}

app.get("/api/hotels", async (req, res) => {
  try { res.json(await searchHotels(req.query)); }
  catch (err) { res.status(500).json({ error: err.message }); }
});

app.listen(3131, () => console.log("Trip agent running on http://localhost:3131"));

The upstream call is StayAPI's Google Travel search endpoint, which returns ~20 hotels per query with prices, ratings, and review counts in one request.
The slice(0, 8) and field trimming matter: the agent only needs 6 fields per hotel, and smaller tool results mean cheaper, faster loops.
Run node --env-file=.env server.js and open the debug route in a browser:

http://localhost:3131/api/hotels?query=hotels+in+Lisbon&check_in=2026-10-08&check_out=2026-10-11&min_rating=4

Browser showing pretty-printed JSON of real Lisbon hotels with names, ratings, review counts, and nightly prices (StayAPI ai travel agent tutorial)

Real hotels, real prices:

[
  {
    "name": "Brown's | Avenue Hotel",
    "rating": 4.6,
    "review_count": 279,
    "stars": 5,
    "price_per_night": "$279",
    "description": "Polished adults-only hotel offering a lounge & a restaurant, plus a rooftop pool with city views."
  },
  ...
]

This is exactly what the agent will see when it calls the tool.
If this page works, everything after it is plumbing.

Step 2: Give the Model Tools

Now the loop.
Describe search_hotels to the model, and keep exchanging messages until it stops requesting tool calls:

const tools = [{
  type: "function",
  function: {
    name: "search_hotels",
    description: "Search live hotels for a destination and stay dates. Returns real hotels with nightly prices, guest ratings, and review counts.",
    parameters: {
      type: "object",
      properties: {
        query: { type: "string", description: "Free-text search, e.g. 'hotels in Lisbon'" },
        check_in: { type: "string", description: "YYYY-MM-DD" },
        check_out: { type: "string", description: "YYYY-MM-DD" },
        min_rating: { type: "number", enum: [3.5, 4, 4.5], description: "Minimum guest rating filter" }
      },
      required: ["query", "check_in", "check_out"]
    }
  }
}];

app.post("/api/plan", async (req, res) => {
  const messages = [
    { role: "system", content: SYSTEM },
    { role: "user", content: req.body.prompt }
  ];
  try {
    for (let round = 0; round < 5; round++) {
      const r = await fetch("https://api.openai.com/v1/chat/completions", {
        method: "POST",
        headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json" },
        body: JSON.stringify({ model: "gpt-5-mini", messages, tools })
      });
      const data = await r.json();
      if (data.error) throw new Error(data.error.message);
      const msg = data.choices[0].message;
      messages.push(msg);
      if (!msg.tool_calls) return res.json({ plan: msg.content });
      for (const call of msg.tool_calls) {
        const result = await searchHotels(JSON.parse(call.function.arguments));
        messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });
      }
    }
    res.status(500).json({ error: "Agent did not finish within 5 rounds" });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

The round < 5 cap is the seatbelt: a confused model can't burn tokens forever.
Our first system prompt was two lines:

You are a travel agent. Before recommending any hotel you MUST call search_hotels; never invent hotels or prices.
Reply with a shortlist of 2-3 real hotels (name, nightly price, rating, review count, one line on why) followed by a day-by-day plan.

And here's the honest part: on one run it worked, and on the next the agent skipped the search and interviewed us instead.

First-run glitch: the agent responds with clarifying questions about dates and budget instead of searching (StayAPI ai travel agent tutorial)

Reasonable behavior for a human travel agent, wrong for a demo app.
Two added lines fixed it, and taught us the pattern: whatever freedom you don't want the model to use, remove in the prompt.

If the request leaves out details, assume 2 adults and pick reasonable dates; search right away instead of asking follow-up questions.
Prefer hotels with at least 100 reviews.

The second line exists because an early run confidently shortlisted a guesthouse with 5 reviews.
Real data needs quality bars, not just truth bars.
With a minimal form and a <pre> tag for output, the grounded agent now answers properly:

Plain-text trip plan listing three real Lisbon hotels with prices, ratings, and review counts, plus a day-by-day itinerary (StayAPI ai travel agent tutorial)

Note the first line of its answer: "I assumed 2 adults and searched for a 3-night stay in Lisbon."
The prompt fix is visibly doing its job.

Step 3: Make It Look Like a Product

A wall of text is a proof of concept; cards are a product.
Two changes get you there.
First, ask the model for JSON instead of prose, using structured outputs (response_format with a JSON schema).
The schema mirrors what the UI renders:

const responseFormat = {
  type: "json_schema",
  json_schema: {
    name: "trip_plan", strict: true,
    schema: {
      type: "object", additionalProperties: false,
      properties: {
        destination: { type: "string" },
        hotels: { type: "array", items: { /* name, price_per_night, rating, review_count, why */ } },
        days: { type: "array", items: { /* day, title, morning, afternoon, evening */ } }
      },
      required: ["destination", "hotels", "days"]
    }
  }
};

The loop line becomes return res.json(JSON.parse(msg.content)), and a real response comes back shaped exactly like the schema:

{
  "destination": "Lisbon",
  "hotels": [
    {
      "name": "Upon Lisbon Prime Residences",
      "price_per_night": "$122",
      "rating": 4.5,
      "review_count": 1839,
      "why": "Great central location with apartment-style rooms, rooftop infinity pool and strong guest scores — excellent mid-range value for couples."
    },
    ...
  ],
  "days": [
    {
      "day": 1,
      "title": "Arrival, Alfama & Baixa",
      "morning": "Check in and settle; head to Praça do Comércio for a riverside stroll and coffee.",
      ...
    },
    ...
  ]
}

Second, replace the <pre> with a render function that maps hotels to cards and days to a schedule (our full index.html is 82 lines including all styling).
The loading state matters more than usual because the agent takes 20-40 seconds:

The app's waiting state: a status line explains the agent is checking live hotel data before answering (StayAPI ai travel agent tutorial)

Submit, wait, and the shortlist arrives with review counts in the four digits, which is precisely what the quality bar in the prompt was for.

The MCP Shortcut

If you want this agent for yourself rather than for your users, you can skip every line of tool-wiring code.
StayAPI ships an MCP server that exposes the same hotel data as ready-made tools for Claude, Cursor, and other MCP clients:

claude mcp add --scope user --transport http stayapi https://api.stayapi.com/mcp --header "X-API-Key: YOUR_API_KEY"

After that, "plan me 3 nights in Lisbon with real prices" works directly in Claude, with the model calling the same search behind the scenes (setup per client is in the MCP documentation).
The tradeoff: MCP puts the agent inside an assistant you already use, while the HTTP API pattern in this tutorial is what you deploy inside your own product.

Troubleshooting

Every row below was triggered for real during this build:

Symptom Likely cause Fix
SyntaxError: Cannot use import statement Missing "type": "module" Add it to package.json
Error: listen EADDRINUSE ... :3131 Port already taken (another server, or the last run still alive) Kill the old process or change the port
HTTP 422, "Field required" on x-api-key The env var didn't load Check .env name and the --env-file=.env flag
HTTP 401, INVALID_API_KEY_FORMAT Placeholder or truncated key Paste the full key from your dashboard
Agent asks questions instead of searching Prompt allows follow-ups Add the "assume and search right away" line

Taking It Further

The single-tool agent is a foundation.
Natural next tools from the same API: hotel reviews (let the agent justify picks with guest sentiment), price calendars (let it suggest cheaper dates), and restaurant search for the food half of the itinerary.
Each one is the same recipe: describe the endpoint as a tool, execute it server-side.
The no-code version of this exists too: N8N and Make.com both run tool-calling AI steps, so the same search-then-plan flow works as a visual workflow.
For a walkthrough of the data side with your own use case, book a demo.

FAQ

Can AI be a travel agent?

For research and planning, yes, if it's grounded in live data.
An LLM alone invents hotels and prices; the same LLM with a search tool recommends real ones.
Booking and payment still need human confirmation or dedicated integrations.

How do I build an AI travel agent from scratch?

Three pieces: a server that can fetch live hotel data, a tool definition that describes that fetch to an LLM, and a loop that executes the model's tool calls until it answers.
This tutorial's version is about 200 lines across two files.

Do I need my own hotel database?

No.
Hotel data APIs return live prices, ratings, and availability per request, so there's nothing to store or sync.
StayAPI's search endpoint returns about 20 hotels per query with the fields an agent needs.

What does it cost to run?

Each plan in our build made 1 hotel-data request and 2 LLM calls (roughly 20-40 seconds end to end).
With a small model like gpt-5-mini, the LLM side typically costs a few cents per plan; check current provider pricing since rates change.

Can I build an AI trip planner without code?

Yes.
No-code platforms like N8N and Make.com support AI steps with tool calling, so you can wire a hotel-search API into an agent flow visually.
The concepts in this tutorial (grounding, quality bars in the prompt, one tool at a time) apply unchanged.

Ship in 3 days

Ready to simplify your hotel data?

Join other developers using StayAPI to build the next generation of travel applications. Get started for free today.