hotelhuddle hotelhuddle API

hotelhuddle API Documentation

Build AI agents, LLM plugins, and travel tools that submit and manage group hotel RFPs on behalf of your users.

Prerequisite: Before you can generate API keys, an account owner must enable API access in Account / Settings and accept the API Access Terms of Service. The Portal > API Keys page becomes available only after both steps are complete.

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:

Supported Platforms

One API key works across the hotelhuddle family of brands. The platform you target is chosen with the brand parameter:

Endpoints Overview

MethodPathDescription
GET/api/v1/cities?q={city}Search cities; returns canonical city ID and metadata.
POST/api/v1/rfp/submitSubmit a group hotel RFP for any platform.
GET/api/v1/rfpsList RFPs submitted with this API key.
GET/api/v1/rfp/{id}Check RFP status, matched hotels, and responses.
POST/api/v1/rfp/{id}/chooseSelect 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

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",
    )

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).

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.