hotelhuddle API Documentation
Build AI agents, LLM plugins, and travel tools that submit and manage group hotel RFPs on behalf of your users.
Base URL: https://api.hotelhuddle.com/api/v1
Authentication: Bearer token in the Authorization header. Generate keys in your Portal > API Keys. API access requires accepting the API Access Terms.
Required LLM Headers: Every request must identify the calling LLM and the end-user's network context:
X-LLM-Identifier— Your product name or model identifier (e.g.openai-gpt-4o,claude-code).X-LLM-Client-IP— The public IP address of the end-user whose behalf you are acting.X-LLM-Client-Location(optional) — Free-form location hint for the end-user, e.g.US-TNorAustin, TX.
Supported Platforms
One API key works across the hotelhuddle family of brands. The platform you target is chosen with the brand parameter:
grouprooms— Group room blockshotelhaggle— Name Your Pricehotelslots— Day-use roomsbookmyteam— Team / multi-destination travelbookmymeeting— Meeting rooms
Endpoints Overview
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/cities?q={city} | Search cities; returns canonical city ID and metadata. |
| POST | /api/v1/rfp/submit | Submit a group hotel RFP for any platform. |
| GET | /api/v1/rfps | List RFPs submitted with this API key. |
| GET | /api/v1/rfp/{id} | Check RFP status, matched hotels, and responses. |
| POST | /api/v1/rfp/{id}/choose | Select a winning hotel offer. |
1. Search Cities
Resolve free-form city text into a canonical city record before submitting an RFP.
GET /api/v1/cities?q=Chattanooga
Authorization: Bearer {api_key}
X-LLM-Identifier: openai-gpt-4o
X-LLM-Client-IP: 203.0.113.45
X-LLM-Client-Location: US-TN
Response:
{
"success": true,
"data": [
{
"id": "Chattanooga-tn-us",
"city": "Chattanooga",
"state": "TN",
"country": "US",
"lat": "35.0456300",
"lng": "-85.3096800"
}
]
}
2. Submit an RFP
Submit a request on behalf of the API key owner. Customer contact details are taken from the account; do not send them in the request body.
POST /api/v1/rfp/submit
Authorization: Bearer {api_key}
X-LLM-Identifier: openai-gpt-4o
X-LLM-Client-IP: 203.0.113.45
Content-Type: application/json
{
"brand": "grouprooms",
"city_id": "Chattanooga-tn-us",
"check_in": "2026-09-15",
"check_out": "2026-09-17",
"king_rooms": 10,
"double_rooms": 5,
"group_name": "Q3 Sales Kickoff",
"terms_agree": true
}
Response:
{
"success": true,
"data": {
"itinerary_number": null,
"brand": "grouprooms",
"rfps": [
{
"id": 2917521,
"city": "Chattanooga",
"status": "submitted"
}
]
}
}
3. Check Status
GET /api/v1/rfp/2917521
Authorization: Bearer {api_key}
X-LLM-Identifier: openai-gpt-4o
X-LLM-Client-IP: 203.0.113.45
Response:
{
"success": true,
"data": {
"id": 2917521,
"status": "submitted",
"brand": "group",
"city": "Chattanooga",
"check_in": "2026-09-15",
"check_out": "2026-09-17",
"matched_hotels": 0,
"responses": [],
"itinerary_number": null,
"stops": []
}
}
4. Choose a Winner
POST /api/v1/rfp/2917521/choose
Authorization: Bearer {api_key}
X-LLM-Identifier: openai-gpt-4o
X-LLM-Client-IP: 203.0.113.45
Content-Type: application/json
{
"hotel_id": "h_12345",
"note": "Confirmed with traveler."
}
Response:
{
"success": true,
"data": {
"rfp_id": 2917521,
"hotel_id": "h_12345",
"status": "accepted"
}
}
5. Multi-Destination (bookmyteam)
For team travel with multiple stops, include a destinations array. The primary stop is defined by the top-level fields; each additional stop in destinations requires its own city_id, dates, and room counts.
{
"brand": "bookmyteam",
"city_id": "Chattanooga-tn-us",
"check_in": "2026-09-15",
"check_out": "2026-09-17",
"king_rooms": 10,
"double_rooms": 5,
"terms_agree": true,
"destinations": [
{
"city_id": "Atlanta-ga-us",
"check_in": "2026-09-18",
"check_out": "2026-09-20",
"king_rooms": 8,
"double_rooms": 4
},
{
"city_id": "Nashville-tn-us",
"check_in": "2026-09-21",
"check_out": "2026-09-23",
"king_rooms": 6,
"double_rooms": 3
}
]
}
6. Platform-Specific Fields
hotelhaggle: includetarget_rate_per_night(USD, e.g.199.99).hotelslots: includetime_window(one of: morning, afternoon, evening, full_day, midday, extended_day, afternoon_evening, overnight_adjacent).bookmymeeting: includeroom_preferenceif desired.
Errors
All errors follow this shape:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR | UNAUTHORIZED | FORBIDDEN | RATE_LIMIT | INTERNAL_ERROR",
"message": "Human-readable description."
}
}
HTTP 429 is returned when you exceed the per-key rate limit (30 city searches/minute, 60 other requests/minute by default).
7. Python Usage Examples
Below is a complete, minimal Python helper for the hotelhuddle API. It includes city search, RFP submission, status polling, and choosing a winner.
7.1 Configuration
import os
import time
import requests
API_KEY = os.environ.get("HOTELHUDDLE_API_KEY", "your_api_key_here")
LLM_ID = os.environ.get("HOTELHUDDLE_LLM_ID", "my-llm-agent")
BASE_URL = "https://api.hotelhuddle.com/api/v1"
def headers(end_user_ip: str, end_user_location: str = ""):
"""Build the required authentication and LLM transparency headers."""
h = {
"Authorization": f"Bearer {API_KEY}",
"X-LLM-Identifier": LLM_ID,
"X-LLM-Client-IP": end_user_ip,
"Content-Type": "application/json",
}
if end_user_location:
h["X-LLM-Client-Location"] = end_user_location
return h
7.2 Search for a City
def search_city(query: str, end_user_ip: str, end_user_location: str = "") -> dict:
url = f"{BASE_URL}/cities"
params = {"q": query}
resp = requests.get(url, params=params, headers=headers(end_user_ip, end_user_location))
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
raise RuntimeError(data.get("error", {}).get("message", "City search failed"))
return data["data"]
# Example
matches = search_city("Chattanooga", end_user_ip="203.0.113.45")
print(matches)
# [{'id': 'Chattanooga-tn-us', 'city': 'Chattanooga', 'state': 'TN', ...}]
7.3 Submit an RFP
def submit_rfp(
brand: str,
city_id: str,
check_in: str,
check_out: str,
king_rooms: int,
double_rooms: int,
end_user_ip: str,
end_user_location: str = "",
**extras
) -> dict:
payload = {
"brand": brand,
"city_id": city_id,
"check_in": check_in,
"check_out": check_out,
"king_rooms": king_rooms,
"double_rooms": double_rooms,
"terms_agree": True,
}
payload.update(extras)
resp = requests.post(
f"{BASE_URL}/rfp/submit",
json=payload,
headers=headers(end_user_ip, end_user_location),
)
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
raise RuntimeError(data.get("error", {}).get("message", "RFP submit failed"))
return data["data"]
# Example — single-destination group block
result = submit_rfp(
brand="grouprooms",
city_id="Chattanooga-tn-us",
check_in="2026-09-15",
check_out="2026-09-17",
king_rooms=10,
double_rooms=5,
group_name="Q3 Sales Kickoff",
end_user_ip="203.0.113.45",
)
print(result)
# {'itinerary_number': None, 'brand': 'grouprooms', 'rfps': [{'id': 12345, ...}]}
7.4 Multi-Destination RFP
def submit_multi_destination(
primary_city_id: str,
primary_check_in: str,
primary_check_out: str,
destinations: list[dict],
end_user_ip: str,
**primary_fields
) -> dict:
payload = {
"brand": "bookmyteam",
"city_id": primary_city_id,
"check_in": primary_check_in,
"check_out": primary_check_out,
"destinations": destinations,
"terms_agree": True,
}
payload.update(primary_fields)
resp = requests.post(
f"{BASE_URL}/rfp/submit",
json=payload,
headers=headers(end_user_ip),
)
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
raise RuntimeError(data.get("error", {}).get("message", "Multi-destination submit failed"))
return data["data"]
# Example — three-stop team trip
multi = submit_multi_destination(
primary_city_id="Chattanooga-tn-us",
primary_check_in="2026-09-15",
primary_check_out="2026-09-17",
destinations=[
{
"city_id": "Atlanta-ga-us",
"check_in": "2026-09-18",
"check_out": "2026-09-20",
"king_rooms": 8,
"double_rooms": 4,
},
{
"city_id": "Nashville-tn-us",
"check_in": "2026-09-21",
"check_out": "2026-09-23",
"king_rooms": 6,
"double_rooms": 3,
},
],
king_rooms=10,
double_rooms=5,
group_name="Road Trip 2026",
end_user_ip="203.0.113.45",
)
print(multi)
# {'itinerary_number': 'abc123...', 'brand': 'bookmyteam', 'rfps': [...]}
7.5 Check RFP Status
def get_rfp(rfp_id: int, end_user_ip: str) -> dict:
resp = requests.get(
f"{BASE_URL}/rfp/{rfp_id}",
headers=headers(end_user_ip),
)
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
raise RuntimeError(data.get("error", {}).get("message", "Status check failed"))
return data["data"]
# Example
status = get_rfp(12345, end_user_ip="203.0.113.45")
print(status)
# {'id': 12345, 'status': 'submitted', 'responses': [], ...}
7.6 Poll Until Hotels Respond
def wait_for_responses(rfp_id: int, end_user_ip: str, max_minutes: int = 30, interval: int = 60) -> list:
"""Poll the RFP status until at least one hotel response arrives or timeout hits."""
deadline = time.time() + (max_minutes * 60)
while time.time() < deadline:
status = get_rfp(rfp_id, end_user_ip)
responses = status.get("responses", [])
if responses:
return responses
time.sleep(interval)
return []
responses = wait_for_responses(12345, end_user_ip="203.0.113.45")
print(responses)
# [{'hotel_id': 'h_12345', 'hotel_name': 'Hotel One', 'rate_cents': 15999, 'currency': 'USD'}]
7.7 Choose a Winning Hotel
def choose_winner(rfp_id: int, hotel_id: str, end_user_ip: str, note: str = "") -> dict:
payload = {"hotel_id": hotel_id}
if note:
payload["note"] = note
resp = requests.post(
f"{BASE_URL}/rfp/{rfp_id}/choose",
json=payload,
headers=headers(end_user_ip),
)
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
raise RuntimeError(data.get("error", {}).get("message", "Choose winner failed"))
return data["data"]
# Example — choose the cheapest responded hotel
cheapest = min(responses, key=lambda r: r["rate_cents"])
choose_winner(12345, cheapest["hotel_id"], end_user_ip="203.0.113.45", note="Best value")
7.8 List RFPs
def list_rfps(end_user_ip: str, limit: int = 20, offset: int = 0) -> dict:
resp = requests.get(
f"{BASE_URL}/rfps",
params={"limit": limit, "offset": offset},
headers=headers(end_user_ip),
)
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
raise RuntimeError(data.get("error", {}).get("message", "List RFPs failed"))
return data["data"]
rfps = list_rfps(end_user_ip="203.0.113.45")
print(rfps)
# {'total': 5, 'limit': 20, 'offset': 0, 'items': [{'id': 12345, 'status': 'submitted', 'ai': True}, ...]}
7.9 Full End-to-End Example
def book_group_block(city_query: str, check_in: str, check_out: str, rooms: dict, end_user_ip: str):
# 1. Resolve city
cities = search_city(city_query, end_user_ip)
if not cities:
raise ValueError(f"No city found for {city_query}")
city = cities[0]
# 2. Submit RFP
rfp = submit_rfp(
brand="grouprooms",
city_id=city["id"],
check_in=check_in,
check_out=check_out,
king_rooms=rooms.get("king", 0),
double_rooms=rooms.get("double", 0),
end_user_ip=end_user_ip,
group_name="LLM-booked group block",
)
rfp_id = rfp["rfps"][0]["id"]
print(f"Submitted RFP {rfp_id}")
# 3. Wait for responses (production: run as background job)
responses = wait_for_responses(rfp_id, end_user_ip, max_minutes=2)
if not responses:
print("No responses yet; check back later.")
return rfp_id
# 4. Choose cheapest
best = min(responses, key=lambda r: r["rate_cents"])
choose_winner(rfp_id, best["hotel_id"], end_user_ip, note="Selected by LLM agent")
print(f"Selected hotel {best['hotel_id']} at {best['rate_cents']} cents/night")
return rfp_id
if __name__ == "__main__":
book_group_block(
city_query="Chattanooga",
check_in="2026-09-15",
check_out="2026-09-17",
rooms={"king": 10, "double": 5},
end_user_ip="203.0.113.45",
)
AI-Submitted Identifier
Every RFP created through this API is tagged with ai_submitted=1, source_domain=api.hotelhuddle.com, and the originating api_key_id. You can identify them in your dashboard and in the list endpoint via the ai flag.