Travel
Duffel API client for Elixir. Search and book flights and hotels through a clean, idiomatic interface built on Req.
Installation
Add :travel to your dependencies:
def deps do
[
{:travel, "~> 0.1.0"}
]
endRequires a Duffel API access token. Get one at app.duffel.com.
Quick Start
# Configure the client
config = Travel.new(access_token: "duffel_test_...")
# Search for hotels in London
{:ok, response} = Travel.Stays.Search.search(config, %{
location: %{
geographic_coordinates: %{latitude: 51.5074, longitude: -0.1278},
radius: 10
},
check_in_date: "2026-08-01",
check_out_date: "2026-08-03",
rooms: 1,
guests: [%{type: "adult"}]
})
response.data.results |> Enum.each(fn result ->
IO.puts("#{result.accommodation.name} - #{result.cheapest_rate_total_amount} #{result.cheapest_rate_currency}")
end)
# Search for flights
{:ok, response} = Travel.Flights.OfferRequests.create(config, %{
slices: [
%{origin: "LHR", destination: "JFK", departure_date: "2026-08-01"}
],
passengers: [%{type: "adult"}]
}, %{return_offers: true})
IO.puts("Found #{length(response.data.offers)} offers")Configuration
config = Travel.new(
access_token: "duffel_test_...", # required
base_url: "https://api.duffel.com", # optional, default
api_version: "v2", # optional, default
debug: false # optional, default
)Stays API
Search
Search by location or by specific accommodation IDs:
# Location-based search
{:ok, response} = Travel.Stays.Search.search(config, %{
location: %{
geographic_coordinates: %{latitude: 51.5, longitude: -0.1},
radius: 10
},
check_in_date: "2026-08-01",
check_out_date: "2026-08-03",
rooms: 1,
guests: [%{type: "adult"}, %{type: "child", age: 8}]
})
# Accommodation-based search
{:ok, response} = Travel.Stays.Search.search(config, %{
accommodation: %{
ids: ["acc_0000AZ2OJbCJNYH4Y2Zm5j"],
fetch_rates: true
},
check_in_date: "2026-08-01",
check_out_date: "2026-08-03",
rooms: 1,
guests: [%{type: "adult"}]
})Search Results
Fetch all rates for a search result:
{:ok, response} = Travel.Stays.SearchResults.fetch_all_rates(config, "ser_123")Quotes
Create and retrieve quotes:
# Create a quote from a rate
{:ok, response} = Travel.Stays.Quotes.create(config, "rate_123")
# Get a quote
{:ok, response} = Travel.Stays.Quotes.get(config, "quo_123")Bookings
Create, retrieve, list, and cancel bookings:
# Create a booking
{:ok, response} = Travel.Stays.Bookings.create(config, %{
quote_id: "quo_123",
guests: [%{given_name: "John", family_name: "Smith"}],
email: "john@example.com",
phone_number: "+447700900000",
loyalty_programme_account_number: "123456789", # optional
accommodation_special_requests: "Late check-in" # optional
})
# Get a booking
{:ok, response} = Travel.Stays.Bookings.get(config, "bok_123")
# List bookings (paginated)
{:ok, response} = Travel.Stays.Bookings.list(config, %{limit: 20})
# Stream all bookings (handles pagination automatically)
Travel.Stays.Bookings.stream(config)
|> Enum.each(fn response ->
Enum.each(response.data, fn booking ->
IO.puts("#{booking.id} - #{booking.status}")
end)
end)
# Cancel a booking
{:ok, response} = Travel.Stays.Bookings.cancel(config, "bok_123")Accommodation
# Get by ID
{:ok, response} = Travel.Stays.Accommodation.get(config, "acc_123")
# List near a location
{:ok, response} = Travel.Stays.Accommodation.list(config, %{
latitude: 51.5,
longitude: -0.1,
radius: 5
})
# Get suggestions
{:ok, response} = Travel.Stays.Accommodation.suggestions(config, "Hilton London")
# Get suggestions with location filter
{:ok, response} = Travel.Stays.Accommodation.suggestions(config, "Hilton", %{
radius: 10,
geographic_coordinates: %{latitude: 51.5, longitude: -0.1}
})
# Get reviews
{:ok, response} = Travel.Stays.Accommodation.reviews(config, "acc_123", %{limit: 10})Brands
# List all brands
{:ok, response} = Travel.Stays.Brands.list(config)
# Get a brand
{:ok, response} = Travel.Stays.Brands.get(config, "brd_123")Loyalty Programmes
{:ok, response} = Travel.Stays.LoyaltyProgrammes.list(config)Flights API
Offer Requests
Create flight searches and retrieve offers:
# Create and return offers
{:ok, response} = Travel.Flights.OfferRequests.create(config, %{
slices: [
%{origin: "LHR", destination: "JFK", departure_date: "2026-08-01"},
%{origin: "JFK", destination: "LHR", departure_date: "2026-08-15"}
],
passengers: [
%{type: "adult"},
%{type: "child", age: 8}
],
cabin_class: "economy"
}, %{return_offers: true})
# Get an offer request
{:ok, response} = Travel.Flights.OfferRequests.get(config, "orq_123")
# List offer requests
{:ok, response} = Travel.Flights.OfferRequests.list(config, %{limit: 20})Offers
# List offers for an offer request
{:ok, response} = Travel.Flights.Offers.list(config, "orq_123")
# Get a specific offer
{:ok, response} = Travel.Flights.Offers.get(config, "off_123", %{
return_available_services: true
})
# Update passenger details
{:ok, response} = Travel.Flights.Offers.update(config, "off_123", "pas_123", %{
given_name: "John",
family_name: "Smith",
loyalty_programme_accounts: [%{account_number: "123456", airline_iata_code: "BA"}]
})
# Price an offer
{:ok, response} = Travel.Flights.Offers.get_priced(config, "off_123", %{
intended_payment_methods: [%{type: "balance"}],
intended_services: []
})Orders
# Create an order
{:ok, response} = Travel.Flights.Orders.create(config, %{
selected_offers: ["off_123"],
passengers: [
%{
given_name: "John",
family_name: "Smith",
born_on: "1990-01-01",
gender: "m",
title: "mr",
email: "john@example.com",
phone_number: "+447700900000"
}
],
type: "instant",
metadata: %{"customer_ref" => "ABC123"}
})
# Get an order
{:ok, response} = Travel.Flights.Orders.get(config, "ord_123")
# List orders
{:ok, response} = Travel.Flights.Orders.list(config, %{
awaiting_payment: true
})
# Update order metadata
{:ok, response} = Travel.Flights.Orders.update(config, "ord_123", %{
metadata: %{"payment_intent_id" => "pit_123"}
})
# Get available services
{:ok, response} = Travel.Flights.Orders.get_available_services(config, "ord_123")
# Add services (baggage, seats)
{:ok, response} = Travel.Flights.Orders.add_services(config, "ord_123", %{
payment: %{type: "balance", amount: "30.00", currency: "GBP"},
add_services: [%{id: "asr_123", quantity: 1}]
})Payments
# Pay for a pay-later order
{:ok, response} = Travel.Flights.Payments.create(config, %{
order_id: "ord_123",
payment: %{type: "balance", amount: "150.00", currency: "GBP"}
})Seat Maps
{:ok, response} = Travel.Flights.SeatMaps.get(config, %{offer_id: "off_123"})Order Cancellations
# Create a cancellation
{:ok, response} = Travel.Flights.OrderCancellations.create(config, %{
order_id: "ord_123"
})
# Get a cancellation
{:ok, response} = Travel.Flights.OrderCancellations.get(config, "ore_123")
# List cancellations
{:ok, response} = Travel.Flights.OrderCancellations.list(config, %{order_id: "ord_123"})
# Confirm a cancellation
{:ok, response} = Travel.Flights.OrderCancellations.confirm(config, "ore_123")Order Changes
# Create a change request
{:ok, response} = Travel.Flights.OrderChangeRequests.create(config, %{
order_id: "ord_123",
slices: %{
add: [%{origin: "LHR", destination: "CDG", departure_date: "2026-09-01"}],
remove: [%{slice_id: "sli_123"}]
}
})
# List change offers for a change request
{:ok, response} = Travel.Flights.OrderChangeOffers.list(config, %{
order_change_request_id: "ocr_123"
})
# Create an order change
{:ok, response} = Travel.Flights.OrderChanges.create(config, %{
selected_order_change_offer: "oco_123"
})
# Confirm the change (without payment)
{:ok, response} = Travel.Flights.OrderChanges.confirm(config, "orc_123")
# Confirm with payment
{:ok, response} = Travel.Flights.OrderChanges.confirm(config, "orc_123", %{
payment: %{type: "balance", amount: "50.00", currency: "GBP"}
})Batch Offer Requests
For long-polling searches that return offers incrementally:
# Create
{:ok, response} = Travel.Flights.BatchOfferRequests.create(config, %{
slices: [...],
passengers: [...]
})
# Poll for results
{:ok, response} = Travel.Flights.BatchOfferRequests.get(config, "bor_123")Partial Offer Requests
For multi-step search flows:
{:ok, response} = Travel.Flights.PartialOfferRequests.create(config, %{
slices: [...],
passengers: [...]
})
{:ok, response} = Travel.Flights.PartialOfferRequests.get(config, "por_123", %{
selected_partial_offer: "off_123"
})
# Get fares for a partial offer request
{:ok, response} = Travel.Flights.PartialOfferRequests.get_fares_by_id(config, "por_123", %{
selected_partial_offer: "off_456"
})Airline-Initiated Changes
# List changes for an order
{:ok, response} = Travel.Flights.AirlineInitiatedChanges.list(config, %{order_id: "ord_123"})
# Accept a change
{:ok, response} = Travel.Flights.AirlineInitiatedChanges.accept(config, "aic_123")
# Update with action taken
{:ok, response} = Travel.Flights.AirlineInitiatedChanges.update(config, "aic_123", %{
action_taken: "accepted"
})Airline Credits
# Create an airline credit
{:ok, response} = Travel.Flights.AirlineCredits.create(config, %{
airline_iata_code: "BA",
amount: "100.00",
amount_currency: "GBP",
code: "1234567890123",
type: "eticket",
issued_on: "2026-01-15",
expires_at: "2027-01-15T00:00:00Z"
})
# Get an airline credit
{:ok, response} = Travel.Flights.AirlineCredits.get(config, "acd_123")
# List airline credits
{:ok, response} = Travel.Flights.AirlineCredits.list(config, %{user_id: "icu_123"})Response Format
All functions return {:ok, response} | {:error, error} tuples.
Success Response
{:ok, %Travel.Types.DuffelResponse{
data: %Travel.Stays.Types.StaysBooking{...},
meta: %Travel.Types.PaginationMeta{limit: 20, after: "cursor_abc", before: nil},
status: 200,
headers: %{"x-request-id" => ["req_123"]}
}}Error Response
{:error, %Travel.Error{
status: 400,
code: "invalid_request",
message: "Field 'check_in_date' must be after today",
title: "Bad Request",
type: "validation_error",
request_id: "req_123",
documentation_url: "https://duffel.com/docs/api/errors"
}}Pagination
List endpoints support cursor-based pagination:
# Single page
{:ok, response} = Travel.Stays.Bookings.list(config, %{limit: 20, after: "cursor_abc"})
# Auto-paginating stream
Travel.Stays.Bookings.stream(config)
|> Stream.flat_map(& &1.data)
|> Enum.to_list()Testing
mix testAll tests use Bypass for HTTP mocking — no network or API credentials required.
License
MIT