# duffelbag

An unofficial Python client library for the [Duffel](https://duffel.com) travel booking API.

Duffel provides a single API for searching and booking flights across hundreds of airlines, hotel stays, and managing payments. **duffelbag** wraps this API with a typed, Pythonic interface built on [httpx](https://www.python-httpx.org/) and [Pydantic v2](https://docs.pydantic.dev/).

## Features

- Sync and async clients with identical API surfaces
- Pydantic v2 models for all request and response types
- Automatic pagination with lazy iterators
- Built-in rate limit handling (auto-retry on 429)
- Typed exceptions mapped to HTTP status codes
- Covers flights, stays, payments, webhooks, and identity APIs

## Installation

```bash
pip install duffelbag
```

Requires Python 3.12+.

## Quick start

Sign up at [duffel.com](https://duffel.com) and grab your API token from the dashboard. Test tokens start with `duffel_test_` and let you make bookings against Duffel's simulated airlines at no cost.

### Search for flights

```python
from duffelbag import Duffel

client = Duffel(token="duffel_test_...")

offer_request = client.offer_requests.create(
    slices=[{
        "origin": "LHR",
        "destination": "JFK",
        "departure_date": "2026-06-15",
    }],
    passengers=[{"type": "adult"}],
    cabin_class="economy",
    return_offers=True,
)

for offer in sorted(offer_request.offers, key=lambda o: float(o.total_amount))[:5]:
    print(f"{offer.owner.name:20s}  {offer.total_amount} {offer.total_currency}")

client.close()
```

### Book a flight

```python
from duffelbag import Duffel

client = Duffel(token="duffel_test_...")

# 1. Search
offer_request = client.offer_requests.create(
    slices=[{"origin": "LHR", "destination": "JFK", "departure_date": "2026-06-15"}],
    passengers=[{"type": "adult"}],
    return_offers=True,
)

# 2. Pick an offer
offer = sorted(offer_request.offers, key=lambda o: float(o.total_amount))[0]

# 3. Create an order
order = client.orders.create(
    selected_offers=[offer.id],
    passengers=[{
        "id": offer.passengers[0].id,
        "title": "mr",
        "gender": "m",
        "given_name": "Test",
        "family_name": "Traveller",
        "born_on": "1990-01-01",
        "email": "test@example.com",
        "phone_number": "+442080000000",
    }],
    type="instant",
    payments=[{
        "type": "balance",
        "amount": offer.total_amount,
        "currency": offer.total_currency,
    }],
)

print(f"Booked! Reference: {order.booking_reference}")
client.close()
```

### Async usage

```python
import asyncio
from duffelbag import AsyncDuffel

async def main():
    async with AsyncDuffel(token="duffel_test_...") as client:
        offer_request = await client.offer_requests.create(
            slices=[{"origin": "LHR", "destination": "CDG", "departure_date": "2026-07-01"}],
            passengers=[{"type": "adult"}],
            return_offers=True,
        )

        async for offer in client.offers.list(offer_request.id, sort="total_amount"):
            print(f"{offer.owner.name}  {offer.total_amount} {offer.total_currency}")

asyncio.run(main())
```

### Browse reference data

```python
from duffelbag import Duffel

client = Duffel(token="duffel_test_...")

# Airlines, airports, aircraft, cities — .page() fetches one page
for airline in client.airlines.list(limit=10).page():
    print(f"[{airline.iata_code}] {airline.name}")

# Place search
for place in client.places.suggest("Tokyo"):
    print(f"{place.type}: {place.name} ({place.iata_code})")

client.close()
```

## API coverage

| Section | Resources |
|---------|-----------|
| **Flights** | Offer requests, offers, orders, order changes, order cancellations, seat maps, payments, partial offer requests, batch offer requests, airline-initiated changes, airline credits |
| **Stays** | Search, quotes, bookings, accommodation, brands, negotiated rates, loyalty programmes |
| **Payments** | Cards (PCI-compliant endpoint), 3D Secure sessions |
| **Notifications** | Webhooks, webhook events, webhook deliveries |
| **Identity** | Customer users, customer user groups, component client keys |
| **Reference data** | Airlines, airports, aircraft, cities, places, loyalty programmes |

Every resource is available through both the sync `Duffel` client and the async `AsyncDuffel` client.

## Pagination

List endpoints return lazy iterators that fetch pages on demand:

```python
# Iterate through all results automatically
for airline in client.airlines.list():
    print(airline.name)

# Or fetch one page at a time
iterator = client.airports.list(limit=50, iata_country_code="US")
first_page = iterator.page()
second_page = iterator.page()
```

Async pagination works with `async for`:

```python
async for offer in client.offers.list(offer_request_id):
    print(offer.total_amount)
```

## Error handling

API errors are raised as typed exceptions:

```python
from duffelbag import Duffel, NotFoundError, ValidationError, AuthenticationError

client = Duffel(token="duffel_test_...")

try:
    client.orders.get("ord_nonexistent")
except NotFoundError as e:
    print(f"Not found: {e}")
except ValidationError as e:
    print(f"Validation error: {e}")
    for error in e.errors:
        print(f"  {error.code}: {error.message}")
except AuthenticationError:
    print("Check your API token")
```

## Examples

See the [`examples/`](examples/) directory for runnable scripts:

- **[search_flights.py](examples/search_flights.py)** -- Search and display flight offers
- **[book_and_cancel_flight.py](examples/book_and_cancel_flight.py)** -- Full booking lifecycle
- **[async_search.py](examples/async_search.py)** -- Async round-trip flight search
- **[explore_reference_data.py](examples/explore_reference_data.py)** -- Browse airlines, airports, and places
- **[seat_maps.py](examples/seat_maps.py)** -- Fetch seat map data for an offer

Set your token via environment variable or a `test_api_key` file:

```bash
export DUFFEL_TOKEN="duffel_test_..."
python examples/search_flights.py
```

## Duffel API documentation

- [API overview](https://duffel.com/docs/api/overview/welcome)
- [Flights guide](https://duffel.com/docs/guides/getting-started-with-flights)
- [Stays guide](https://duffel.com/docs/guides/getting-started-with-stays)
- [API reference](https://duffel.com/docs/api)

## Development

```bash
# Clone and install
git clone https://github.com/nickovs/duffelbag.git
cd duffelbag
uv pip install -e ".[dev]"

# Run tests (mocked, no API key needed)
.venv/bin/pytest tests/ --ignore=tests/test_live.py -v

# Run live integration tests (requires test_api_key file)
.venv/bin/pytest tests/test_live.py -v

# Lint
.venv/bin/ruff check src/ tests/
```

## License

MIT
