How to Build a Travel Booking Agent With ATXP
Travel booking is one of the highest-value agent use cases—and one of the hardest to build correctly. Not because the search is difficult. Because the payment surface is large.
A travel booking agent that can build travel booking agent ai workflows needs to:
- Search flights, compare prices, and hold options without purchasing
- Enforce a per-task budget at each step, not just a monthly cap
- Handle the multi-step commit sequence (flight → hotel → confirmation) where each step can fail
- Process cancellations and release funds when plans change
- Generate receipts and audit trails that a human can review
This tutorial covers all of it, end to end, with working code. If you want to understand the underlying budget model before diving in, start with Per-Task Budgeting for AI Agents — it explains why monthly caps break in exactly this kind of multi-step workflow.
Why Is Travel Booking a Better Test Than a Simple Purchase?
A single API call is a trivial payment problem. Travel booking is a realistic one.
Here’s the complexity surface you’re dealing with:
| Step | Payment State | What Can Go Wrong |
|---|---|---|
| Flight search | No spend — tool call only | Nothing — but agent must not pre-authorize |
| Price lock / hold | Optional reservation fee | Fee is non-refundable; agent must confirm budget before committing |
| Flight purchase | Full ticket price debited | Card declined, price changed, inventory gone |
| Hotel search | No spend | Nothing |
| Hotel booking | Deposit or full charge | Cancellation policy differs per property |
| Change/cancel | Refund or penalty fee | Partial refund timing varies; agent must track original spend |
Without a per-task payment layer, you’re either handing the agent a credit card with no limits (dangerous) or blocking every step for human approval (defeats the purpose). ATXP’s IOU credit model handles this differently: you fund the task at the start, each spend deducts from that allocation, and anything unspent returns automatically when the task closes.
What Does the Architecture Look Like?
The agent has three categories of tools:
Search tools — no payment, just data. Return structured results. Booking tools — spend from the task budget. Return confirmation and receipt. Management tools — cancel, modify, check status. May trigger refunds or penalty fees.
User: "Book me a flight to Austin Thursday, return Sunday, under $400"
│
├── search_flights(origin, dest, dates, max_price) ← no spend
├── search_hotels(location, dates, max_price) ← no spend
│
├── [agent presents options]
│
├── book_flight(flight_id, passenger_info) ← spends from task budget
├── book_hotel(hotel_id, guest_info) ← spends from task budget
│
├── confirm_booking(trip_id) ← generates receipt
│
└── [on cancel]: cancel_flight(booking_ref) ← returns refund to task budget
The key constraint: the agent can search freely, but it cannot book unless the task budget covers the full amount. ATXP enforces this at the infrastructure level — there’s no code path where the agent accidentally overspends.
Setting Up the ATXP Client
Install the SDK and initialize with your agent’s credentials:
pip install atxp-sdk
from atxp import ATXP, TaskBudget
client = ATXP(agent_id="travel-booking-agent-v1")
# Fund the task before the agent starts working
task = client.create_task(
task_id="trip-austin-2026-04-03",
budget=TaskBudget(
amount=500.00,
currency="USD",
per_step_max=300.00, # no single booking can exceed $300
requires_confirmation_above=200.00 # pause for human approval above this
)
)
Three budget parameters matter here:
amount— total pool for the entire trip taskper_step_max— ceiling on any single purchase within the taskrequires_confirmation_above— trigger a human checkpoint before high-value commits
You can read more about these controls in How ATXP’s IOU Model Caps Agent Spending.
How Do You Write the Search Tools?
Search tools don’t touch payment — they just return data. Keep them lightweight.
import httpx
from atxp.tools import tool
@tool(requires_payment=False)
async def search_flights(
origin: str,
destination: str,
depart_date: str,
return_date: str,
max_price: float
) -> list[dict]:
"""Search available flights. No payment required."""
async with httpx.AsyncClient() as http:
response = await http.get(
"https://api.yourflightprovider.com/search",
params={
"origin": origin,
"destination": destination,
"depart": depart_date,
"return": return_date,
"max_price": max_price,
},
headers={"Authorization": f"Bearer {FLIGHT_API_KEY}"}
)
return response.json()["results"]
@tool(requires_payment=False)
async def search_hotels(
location: str,
check_in: str,
check_out: str,
max_price_per_night: float
) -> list[dict]:
"""Search available hotels. No payment required."""
async with httpx.AsyncClient() as http:
response = await http.get(
"https://api.yourhotelprovider.com/search",
params={
"location": location,
"check_in": check_in,
"check_out": check_out,
"max_pn": max_price_per_night,
}
)
return response.json()["properties"]
The requires_payment=False decorator tells ATXP these tools cannot debit the task budget. If a tool accidentally tries to initiate a spend, ATXP blocks it.
How Do You Handle Per-Task Budget Enforcement for Booking Steps?
Booking tools are where the payment layer actually matters. Each booking call debits the task budget — and ATXP validates the available balance before the merchant call goes out.
from atxp.tools import tool
from atxp.exceptions import InsufficientBudgetError, ConfirmationRequired
@tool(requires_payment=True, task_id_param="task_id")
async def book_flight(
task_id: str,
flight_id: str,
passenger_name: str,
passenger_email: str,
price: float
) -> dict:
"""
Book a flight. Debits from task budget.
Raises InsufficientBudgetError if budget is exceeded.
Raises ConfirmationRequired if price exceeds requires_confirmation_above.
"""
# ATXP validates budget before this code runs
# If validation fails, exception is raised before the API call
async with httpx.AsyncClient() as http:
response = await http.post(
"https://api.yourflightprovider.com/book",
json={
"flight_id": flight_id,
"passenger": {
"name": passenger_name,
"email": passenger_email,
},
"price": price,
}
)
booking = response.json()
return {
"confirmation_number": booking["confirmation"],
"amount_charged": price,
"receipt_url": booking["receipt"],
"cancellation_policy": booking["cancellation_policy"]
}
If the flight costs more than the remaining task budget, InsufficientBudgetError is raised before the merchant API is called. The agent receives a structured error it can handle — typically by informing the user and asking whether to adjust the task budget.
This is the behavior described in What Happens When Your Agent’s Card Gets Declined: the failure is clean, not silent.
Cancellation Handling: Returning Funds to the Task Budget
Cancellations are where most implementations fall apart. The agent needs to:
- Call the provider’s cancel endpoint
- Receive confirmation of the refund amount (which may differ from the original charge due to cancellation fees)
- Credit the returned amount back to the task budget
@tool(requires_payment=True, task_id_param="task_id")
async def cancel_flight(
task_id: str,
confirmation_number: str,
original_charge: float
) -> dict:
"""Cancel a flight booking and return eligible refund to task budget."""
async with httpx.AsyncClient() as http:
response = await http.post(
"https://api.yourflightprovider.com/cancel",
json={"confirmation": confirmation_number}
)
result = response.json()
refund_amount = result.get("refund_amount", 0.0)
cancellation_fee = original_charge - refund_amount
# Return refund to task budget
await client.task_credit(
task_id=task_id,
amount=refund_amount,
reason=f"Flight cancellation refund — confirmation {confirmation_number}"
)
return {
"status": "cancelled",
"original_charge": original_charge,
"refund_amount": refund_amount,
"cancellation_fee": cancellation_fee,
"net_task_budget_impact": -cancellation_fee
}
The client.task_credit() call is important. Without it, the task budget doesn’t reflect the refund and subsequent booking attempts may fail due to apparent insufficient funds.
Receipt Generation and Audit Trail
Every booking step should produce a receipt that a human can audit. ATXP attaches receipts automatically to each task spend, but you can enrich them:
@tool(requires_payment=False, task_id_param="task_id")
async def generate_trip_receipt(task_id: str) -> dict:
"""Generate a full receipt for the completed booking task."""
task_summary = await client.get_task_summary(task_id)
return {
"task_id": task_id,
"total_budget": task_summary.initial_budget,
"total_spent": task_summary.total_spent,
"remaining_budget": task_summary.remaining,
"line_items": [
{
"description": spend.description,
"amount": spend.amount,
"timestamp": spend.timestamp,
"receipt_url": spend.receipt_url,
"status": spend.status
}
for spend in task_summary.spends
],
"refunds": task_summary.credits,
"net_cost": task_summary.total_spent - task_summary.total_credits
}
This receipt is the audit trail your compliance team (or your own debugging session) will thank you for. It maps directly to the framework covered in Your AI Agent Needs an Audit Trail, Not Just a Guardrail.
Wiring It Into an Agent
Here’s how the full agent loop looks in practice:
from atxp import ATXP, TaskBudget
from openai import OpenAI
client = ATXP(agent_id="travel-booking-agent-v1")
llm = OpenAI()
tools = [
search_flights,
search_hotels,
book_flight,
book_hotel,
cancel_flight,
generate_trip_receipt,
]
async def run_travel_agent(user_request: str, budget: float):
task = client.create_task(
task_id=f"trip-{int(time.time())}",
budget=TaskBudget(
amount=budget,
per_step_max=budget * 0.75,
requires_confirmation_above=budget * 0.5
)
)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Task budget: ${budget}. Request: {user_request}"}
]
while True:
response = llm.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=[t.to_openai_schema() for t in tools],
tool_choice="auto"
)
message = response.choices[0].message
if message.tool_calls:
results = await execute_tool_calls(message.tool_calls, task.task_id)
messages.append(message)
messages.append({"role": "tool", "content": str(results)})
else:
# Agent is done
receipt = await generate_trip_receipt(task.task_id)
return {"response": message.content, "receipt": receipt}
ATXP vs. Raw API Keys for This Use Case
The comparison is not subtle.
| Concern | Raw API Keys | Virtual Card per Step | ATXP Task Budget |
|---|---|---|---|
| Spending cap enforcement | None — agent can charge anything | Per-card limit, manual setup | Automatic, declared upfront |
| Cancellation tracking | Manual reconciliation | New card per booking | Task-level credit tracking |
| Audit trail | Build it yourself | Card statement only | Structured per-step receipts |
| Credential exposure | API key in agent context | Card number in agent context | No payment credential in agent context |
| Multi-step budget awareness | None | None | Real-time remaining balance |
| Failure handling | Provider error only | Card decline, no context | Structured error with budget state |
The virtual card problem at agent scale is covered in detail in Why Per-Task Virtual Cards Don’t Scale. For a travel agent that may issue and cancel bookings repeatedly in one session, the virtual card approach becomes unmanageable.
Connecting to ATXP
Register your agent once at atxp.ai, get your agent_id, and install the SDK. All payment tool calls route through ATXP from that point — no separate billing accounts with each travel API provider.
The full developer quickstart is at atxp.ai/docs/quickstart.
FAQ
Does the task budget close automatically when the agent finishes?
Yes. When the agent returns its final response and you call client.close_task() (or it closes automatically on session end), any unspent IOU credits return to your main ATXP balance. You’re only charged for what was actually spent.
What happens if the flight price changes between search and booking?
The agent searches at one price and books at another — this is a standard race condition in travel APIs. ATXP validates budget against the price passed to the booking tool call. If the new price exceeds the per-step max, the call raises InsufficientBudgetError before the merchant charge goes out. The agent should re-search and present updated options.
Can I set different per-step limits for flights vs. hotels?
Not directly at the task level — per_step_max applies to all spending tools. The cleanest pattern is to structure your booking tools to accept a max_price parameter and enforce it in the tool before calling the provider, giving you tool-level control on top of the task-level cap.
How do I handle a partial itinerary — flight booked, hotel booking fails?
The flight is confirmed and the spend is recorded. Your agent should surface the failure state clearly, offer to cancel the flight (triggering the refund flow), or ask the user whether to proceed without a hotel booking. ATXP’s task summary shows you exactly what was spent and what is recoverable — which is the data you need to make that decision automatically or present it to the user.
Does this work with LangChain or CrewAI?
Yes. The ATXP tool decorator generates standard function schemas. See How to Add ATXP to LangChain for the LangChain-specific wiring.