Skip to main content

LetsFG — Your AI agent just learned to book flights.

Server-side engine. Real prices. One function call. Search hundreds of airlines at raw airline prices — $20–$50 cheaper than Booking.com, Kayak, and other OTAs.

GitHub stars PyPI

Two ways to use LetsFG

CLI / SDK (PFS Bearer token) Developer API
Search cost Free (one-time letsfg auth, nothing charged) Prepaid credits
Booking URL 1% fee (min $3) via letsfg.co Direct airline URL, no fee
Speed 60–90 s 2–5 s (discover) · 60–90 s (full)
Setup pip install letsfg && letsfg auth letsfg.co/developers

Want direct airline URLs without any per-booking fee? Use the Developer API — prepaid credits, results in seconds, no per-booking fee.

Install

pip install letsfg

Authenticate once by putting a payment method on file (zero-amount, nothing charged), then search is free and unlimited:

letsfg auth           # one-time card-on-file setup → 90-day Bearer token (nothing charged)
letsfg search LHR BCN 2026-06-15

Search is free. Booking links require unlock (1% fee, min $3) — see Unlocking offer results below.

Authentication

from letsfg import LetsFG

# Register (one-time, no auth needed)
creds = LetsFG.register("my-agent", "agent@example.com")
print(creds["api_key"])  # "trav_xxxxx..." — save this

# Option A: Pass API key directly
bt = LetsFG(api_key="trav_...")

# Option B: Set LETSFG_API_KEY env var, then:
bt = LetsFG()

# Setup payment (required before unlock) — two options:

# Option 1: Stripe test token (for development)
bt.setup_payment(token="tok_visa")

# Option 2: Stripe PaymentMethod ID (from Stripe.js or Elements)
bt.setup_payment(payment_method_id="pm_1234567890")

The API accepts only Stripe-generated tokens or payment_method_id values — raw card numbers are not accepted.

The API key is sent as X-API-Key header on every request. The SDK handles this automatically.

Verify Your Credentials

# Check that auth + payment are working
profile = bt.me()
print(f"Agent: {profile['agent_name']}")
print(f"Payment: {profile.get('payment_status', 'not set up')}")
print(f"Searches: {profile.get('search_count', 0)}")

Auth Failure Recovery

from letsfg import LetsFG, AuthenticationError

try:
    bt = LetsFG(api_key="trav_...")
    flights = bt.search("LHR", "JFK", "2026-04-15")
except AuthenticationError:
    # Key invalid or expired — re-register to get a new one
    creds = LetsFG.register("my-agent", "agent@example.com")
    bt = LetsFG(api_key=creds["api_key"])
    bt.setup_payment(token="tok_visa")  # Re-attach payment on new key
    flights = bt.search("LHR", "JFK", "2026-04-15")

Quick Start (Python)

from letsfg import LetsFG

bt = LetsFG(api_key="trav_...")

# Search flights — FREE
flights = bt.search("GDN", "BER", "2026-03-03")
print(f"{flights.total_results} offers, cheapest: {flights.cheapest.summary()}")

# Unlock booking link (1% fee, min $3, charged via letsfg.co)
unlock = bt.unlock(flights.cheapest.id)
print(f"Confirmed price: {unlock.confirmed_currency} {unlock.confirmed_price}")

# Book — charges the price shown on the offer
booking = bt.book(
    offer_id=flights.cheapest.id,
    passengers=[{
        "id": flights.passenger_ids[0],
        "given_name": "John",
        "family_name": "Doe",
        "born_on": "1990-01-15",
        "gender": "m",
        "title": "mr",
        "email": "john@example.com",
    }],
    contact_email="john@example.com"
)
print(f"PNR: {booking.booking_reference}")

Multi-Passenger Search

# 2 adults + 1 child, round-trip, premium economy
flights = bt.search(
    "LHR", "JFK", "2026-06-01",
    return_date="2026-06-15",
    adults=2,
    children=1,
    cabin_class="W",  # W=premium, M=economy, C=business, F=first
    sort="price",
)

# passenger_ids will be ["pas_0", "pas_1", "pas_2"]
print(f"Passenger IDs: {flights.passenger_ids}")

# Book with details for EACH passenger
booking = bt.book(
    offer_id=unlocked.offer_id,
    passengers=[
        {"id": "pas_0", "given_name": "John", "family_name": "Doe", "born_on": "1990-01-15", "gender": "m", "title": "mr"},
        {"id": "pas_1", "given_name": "Jane", "family_name": "Doe", "born_on": "1992-03-20", "gender": "f", "title": "ms"},
        {"id": "pas_2", "given_name": "Tom", "family_name": "Doe", "born_on": "2018-05-10", "gender": "m", "title": "mr"},
    ],
    contact_email="john@example.com",
)

Resolve Locations

Always resolve city names to IATA codes before searching:

locations = bt.resolve_location("New York")
# [{"iata_code": "JFK", "name": "John F. Kennedy", "type": "airport", "city": "New York"}, ...]

# Use in search
flights = bt.search(locations[0]["iata_code"], "LAX", "2026-04-15")

Working with Search Results

flights = bt.search("LON", "BCN", "2026-04-01", return_date="2026-04-08", limit=50)

# Iterate all offers
for offer in flights.offers:
    print(f"{offer.owner_airline}: {offer.currency} {offer.price}")
    print(f"  Route: {offer.outbound.route_str}")
    print(f"  Duration: {offer.outbound.total_duration_seconds // 3600}h")
    print(f"  Stops: {offer.outbound.stopovers}")
    print(f"  Refundable: {offer.conditions.get('refund_before_departure', 'unknown')}")
    print(f"  Changeable: {offer.conditions.get('change_before_departure', 'unknown')}")

# Filter: direct flights only
direct = [o for o in flights.offers if o.outbound.stopovers == 0]

# Filter: specific airline
ba = [o for o in flights.offers if "British Airways" in o.airlines]

# Filter: refundable only
refundable = [o for o in flights.offers if o.conditions.get("refund_before_departure") == "allowed"]

# Sort by duration
by_duration = sorted(flights.offers, key=lambda o: o.outbound.total_duration_seconds)

# Cheapest offer
print(f"Best: {flights.cheapest.price} {flights.cheapest.currency}")

Error Handling

from letsfg import (
    LetsFG, LetsFGError,
    AuthenticationError, PaymentRequiredError, OfferExpiredError,
)

bt = LetsFG(api_key="trav_...")

# Handle invalid locations
try:
    flights = bt.search("INVALID", "JFK", "2026-04-15")
except LetsFGError as e:
    if e.status_code == 422:
        # Resolve the location first
        locations = bt.resolve_location("London")
        flights = bt.search(locations[0]["iata_code"], "JFK", "2026-04-15")

# Handle payment and expiry
try:
    unlocked = bt.unlock(offer_id)
except PaymentRequiredError:
    print("Run bt.setup_payment() first")
except OfferExpiredError:
    print("Offer expired — search again for fresh results")

# Handle booking failures
try:
    booking = bt.book(offer_id=unlocked.offer_id, passengers=[...], contact_email="...")
except OfferExpiredError:
    print("30-minute window expired — search and unlock again")
except AuthenticationError:
    print("Invalid API key")
except LetsFGError as e:
    print(f"API error ({e.status_code}): {e.message}")
Exception HTTP Code Cause
AuthenticationError 401 Missing or invalid API key
PaymentRequiredError 402 No payment method (call setup_payment())
OfferExpiredError 410 Offer no longer available
LetsFGError any Base class for all API errors

Timeout and Retry Pattern

Full cloud search takes 60–90 s (async polling). Use retry with backoff for transient errors:

import time
from letsfg import LetsFG, LetsFGError

bt = LetsFG()

def search_with_retry(origin, dest, date, max_retries=3):
    """Retry with exponential backoff on rate limit or timeout."""
    for attempt in range(max_retries):
        try:
            return bt.search(origin, dest, date)
        except LetsFGError as e:
            if "429" in str(e) or "rate limit" in str(e).lower():
                wait = 2 ** attempt  # 1s, 2s, 4s
                print(f"Rate limited, waiting {wait}s...")
                time.sleep(wait)
            elif "timeout" in str(e).lower() or "504" in str(e):
                print(f"Timeout, retrying ({attempt + 1}/{max_retries})...")
                time.sleep(1)
            else:
                raise
    raise LetsFGError("Max retries exceeded")

Rate Limits

Endpoint Rate Limit Typical Latency
Search No hard limit (billing is the natural governor) 60–90 s
Resolve location 120 req/min < 1 s
Unlock 20 req/min 2–5 s
Book 10 req/min 3–10 s

Minimizing Unlock Costs

Searching is free and unlimited. Booking goes through POST /api/agent-book. The 1%-of-ticket unlock fee (min $3) exists only on the paid Developer API. Strategy:

# Search multiple dates (free) — compare before unlocking
dates = ["2026-04-01", "2026-04-02", "2026-04-03"]
best = None
for date in dates:
    result = bt.search("LON", "BCN", date)
    if result.offers and (best is None or result.cheapest.price < best[1].price):
        best = (date, result.cheapest)

# Unlock only the winner (1% fee, min $3)
if best:
    unlocked = bt.unlock(best[1].id)
    # Book within 30 minutes (ticket price only)
    booking = bt.book(offer_id=unlocked.offer_id, passengers=[...], contact_email="...")

Quick Start (CLI)

# Auth (one-time — saves Bearer token to ~/.letsfg/config.json)
letsfg auth

# Search (1 adult, one-way, economy — defaults)
letsfg search GDN BER 2026-03-03 --sort price

# Multi-passenger round trip
letsfg search LON BCN 2026-04-01 --return 2026-04-08 --adults 2 --children 1 --cabin M

# Business class, direct flights only
letsfg search JFK LHR 2026-05-01 --adults 3 --cabin C --max-stops 0

# Machine-readable output (for agents)
letsfg search LON BCN 2026-04-01 --json

# Unlock
letsfg unlock off_xxx

# Book
letsfg book off_xxx \
  --passenger '{"id":"pas_xxx","given_name":"John","family_name":"Doe","born_on":"1990-01-15","gender":"m","title":"mr","email":"john@example.com"}' \
  --email john@example.com

# Resolve location
letsfg locations "Berlin"

Search Flags

Flag Short Default Description
--return -r (one-way) Return date YYYY-MM-DD
--adults -a 1 Adults (1–9)
--children 0 Children 2–11 years
--cabin -c (any) M economy, W premium, C business, F first
--max-stops -s 2 Max stopovers (0–4)
--currency EUR Currency code
--limit -l 20 Max results (1–100)
--sort price price or duration
--json -j Raw JSON output

All CLI Commands

Command Description Cost
auth One-time card-on-file setup → 90-day Bearer token. Nothing charged FREE
search Search flights between any two airports FREE
locations Resolve city name to IATA codes FREE
unlock [Developer API only] Unlock offer (confirms price, reveals booking URL) 1% of ticket, min $3
book Book flight (creates real airline PNR) Ticket price
register Register new Developer API key FREE
setup-payment Attach payment card (required for unlock) FREE
me Show agent profile and usage stats FREE

Every command supports --json for machine-readable output.

Environment Variables

Variable Description
LETSFG_BEARER_TOKEN PFS Bearer token (from letsfg auth). Takes priority over ~/.letsfg/config.json.
LETSFG_API_KEY Developer API key (prepaid credits path)
LETSFG_BASE_URL API URL override (default: https://letsfg.co)

How It Works

  1. Search — Free. The server-side engine queries hundreds of airlines and returns real-time offers.
  2. Book — Call POST /api/agent-book with your Bearer token. It returns either a confirmed order or a direct booking link for that exact offer.
  3. Book — Open the direct airline URL and complete the booking on the airline's own site.

Prices are cheaper because we connect directly to airlines — no OTA markup.


Also Available As

  • MCP Server: npx letsfg-mcpnpm
  • JS/TS SDK: npm install letsfgnpm
  • Try without installing: letsfg.co — search instantly in your browser
  • GitHub: LetsFG/LetsFG

Star the repo — we appreciate the support.

License

MIT

🏨 Hotels — new, and live

Your agent can now book hotels, not just flights. Same API key, same card on file.

from letsfg import LetsFG
lfg = LetsFG()

city = lfg.hotel_destinations("Warsaw")[0]
stays = lfg.search_hotels(
    city_id=city["Id"], city_name=city["Name"],
    check_in="2026-11-10", check_out="2026-11-12", adults=2,
)

hotel = stays["hotels"][0]
offer = hotel["offers"][0]
print(hotel["name"], offer["price"], stays["currency"])
# Hotel Gromada Warszawa Centrum 669.86 PLN

booking = lfg.book_hotel_and_wait(
    session_id=stays["session_id"],
    hotel_code=hotel["hotel_code"],
    combination_id_v2=offer["combination_id_v2"],
    expected_price=offer["price"],
    expected_balance=offer["balance_to_supplier"],
    city_id=city["Id"], city_name=city["Name"],
    check_in="2026-11-10", check_out="2026-11-12",
    guests=[{"title": "Mr", "first_name": "Jan", "last_name": "Kowalski"}],
    email="guest@example.com", phone="512345678",
)
print(booking["confirmation"], booking["pay_link"])

How you pay

10% now, the rest to the hotel later. At booking we charge 10% of the price to your card as a reservation fee. The remaining balance is paid directly to the supplier through a pay_link we return — we never hold it.

balance_due_by is the supplier's own auto-cancellation date, not a date we invent. Miss it and the room is released.

The 10% is non-refundable. Cancelling before balance_due_by costs nothing else; after it, the hotel's own cancellation ladder applies and can reach 100%. That ladder ships in the booking's terms, so you can always see the cost before you cancel.

Things worth knowing before you build

  • A card on file is required for every hotel call, including search. That is unusual and it is deliberate: a hotel search opens a real session at the supplier, and booking blocks a real rate. We would rather refuse up front than let you reach the point of commitment and discover you cannot pay. The same card that authorises flight booking authorises hotels — there is no separate hotel signup.
  • Only free-cancellation, pay-later rates are sold. Those are the rates where the balance can safely be settled with the supplier after booking, which is what makes 10%-now/rest-later work at all. You will see fewer results than a metasearch shows you. Every one of them can actually be booked.
  • Booking is asynchronous. book_hotel returns a booking_job_id, not a booking — the real thing takes minutes. Poll hotel_booking(job_id) until status is succeeded or failed, or call book_hotel_and_wait and let the SDK do it. This is not ceremony: it is what makes it impossible to charge a card and then lose the confirmation to a timeout.
  • The fee is charged before the room is committed. A declined card therefore costs nothing to unwind — no reservation exists and nothing is charged.
  • Do not retry a booking blindly. Calling book_hotel twice for the same rate books the room twice and charges two reservation fees.
  • price is what the guest pays. There is no wholesale figure in the response to quote by mistake.

JavaScript

import { LetsFG } from 'letsfg';
const lfg = new LetsFG({ apiKey: process.env.LETSFG_API_KEY });

const [city] = await lfg.hotelDestinations('Warsaw');
const stays = await lfg.searchHotels({
  cityId: city.Id, cityName: city.Name,
  checkIn: '2026-11-10', checkOut: '2026-11-12', adults: 2,
});

const booking = await lfg.bookHotelAndWait({ /* ...offer + guest details... */ });
console.log(booking.confirmation, booking.pay_link);

MCP

Five new tools, in the order you call them: resolve_hotel_citysearch_hotelsbook_hotelget_hotel_bookingcancel_hotel_booking.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

letsfg-2026.5.90.tar.gz (75.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

letsfg-2026.5.90-py3-none-any.whl (50.5 kB view details)

Uploaded Python 3

File details

Details for the file letsfg-2026.5.90.tar.gz.

File metadata

  • Download URL: letsfg-2026.5.90.tar.gz
  • Upload date:
  • Size: 75.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for letsfg-2026.5.90.tar.gz
Algorithm Hash digest
SHA256 e017f8e670a17c218383555ee215f2f38cac8c58a6238ff8e6b6914a129c07a7
MD5 b7109207918109eb9b3d74bf8d992a76
BLAKE2b-256 44d26e477c65c4cd0e85ebeef8ec7a5e2f2a4b161a652963c60af891d0aa3600

See more details on using hashes here.

File details

Details for the file letsfg-2026.5.90-py3-none-any.whl.

File metadata

  • Download URL: letsfg-2026.5.90-py3-none-any.whl
  • Upload date:
  • Size: 50.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for letsfg-2026.5.90-py3-none-any.whl
Algorithm Hash digest
SHA256 e3e9c8406e1eabc72c8e791bd73c14b477e9877ef566640ddcc6a4d5747dcef8
MD5 09aa9345ed7e9f4e5ee0e8443a23fc00
BLAKE2b-256 adaade36809456246584d7ba046411d53fc89c252d68e0c92ccbb5d61fbd9eb9

See more details on using hashes here.

Release history Release notifications | RSS feed

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page