{"id":49891085,"url":"https://github.com/lambdachad/travel","last_synced_at":"2026-05-15T21:13:48.107Z","repository":{"id":353719793,"uuid":"1220426404","full_name":"lambdachad/travel","owner":"lambdachad","description":"Duffel API Client for Elixir","archived":false,"fork":false,"pushed_at":"2026-04-25T07:30:30.000Z","size":66,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-25T08:33:58.148Z","etag":null,"topics":["duffel","elixir"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/travel","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lambdachad.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-24T22:20:03.000Z","updated_at":"2026-04-25T07:32:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/lambdachad/travel","commit_stats":null,"previous_names":["lambdachad/travel"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/lambdachad/travel","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdachad%2Ftravel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdachad%2Ftravel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdachad%2Ftravel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdachad%2Ftravel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lambdachad","download_url":"https://codeload.github.com/lambdachad/travel/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdachad%2Ftravel/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33080725,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-15T20:25:35.270Z","status":"ssl_error","status_checked_at":"2026-05-15T20:25:34.732Z","response_time":103,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["duffel","elixir"],"created_at":"2026-05-15T21:13:47.454Z","updated_at":"2026-05-15T21:13:48.101Z","avatar_url":"https://github.com/lambdachad.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Travel\n\nDuffel API client for Elixir. Search and book flights and hotels through a clean, idiomatic interface built on [Req](https://github.com/wojtekmach/req).\n\n[![Hex.pm](https://img.shields.io/hexpm/v/travel.svg)](https://hex.pm/packages/travel)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/travel)\n[![License](https://img.shields.io/hexpm/l/travel.svg)](https://github.com/lambdachad/travel/blob/main/LICENSE)\n\n## Installation\n\nAdd `:travel` to your dependencies:\n\n```elixir\ndef deps do\n  [\n    {:travel, \"~\u003e 0.1.0\"}\n  ]\nend\n```\n\nRequires a [Duffel API access token](https://duffel.com/docs/api/overview/authentication). Get one at [app.duffel.com](https://app.duffel.com).\n\n## Quick Start\n\n```elixir\n# Configure the client\nconfig = Travel.new(access_token: \"duffel_test_...\")\n\n# Search for hotels in London\n{:ok, response} = Travel.Stays.Search.search(config, %{\n  location: %{\n    geographic_coordinates: %{latitude: 51.5074, longitude: -0.1278},\n    radius: 10\n  },\n  check_in_date: \"2026-08-01\",\n  check_out_date: \"2026-08-03\",\n  rooms: 1,\n  guests: [%{type: \"adult\"}]\n})\n\nresponse.data.results |\u003e Enum.each(fn result -\u003e\n  IO.puts(\"#{result.accommodation.name} - #{result.cheapest_rate_total_amount} #{result.cheapest_rate_currency}\")\nend)\n\n# Search for flights\n{:ok, response} = Travel.Flights.OfferRequests.create(config, %{\n  slices: [\n    %{origin: \"LHR\", destination: \"JFK\", departure_date: \"2026-08-01\"}\n  ],\n  passengers: [%{type: \"adult\"}]\n}, %{return_offers: true})\n\nIO.puts(\"Found #{length(response.data.offers)} offers\")\n```\n\n## Configuration\n\n```elixir\nconfig = Travel.new(\n  access_token: \"duffel_test_...\",   # required\n  base_url: \"https://api.duffel.com\", # optional, default\n  api_version: \"v2\",                  # optional, default\n  debug: false                        # optional, default\n)\n```\n\n## Stays API\n\n### Search\n\nSearch by location or by specific accommodation IDs:\n\n```elixir\n# Location-based search\n{:ok, response} = Travel.Stays.Search.search(config, %{\n  location: %{\n    geographic_coordinates: %{latitude: 51.5, longitude: -0.1},\n    radius: 10\n  },\n  check_in_date: \"2026-08-01\",\n  check_out_date: \"2026-08-03\",\n  rooms: 1,\n  guests: [%{type: \"adult\"}, %{type: \"child\", age: 8}]\n})\n\n# Accommodation-based search\n{:ok, response} = Travel.Stays.Search.search(config, %{\n  accommodation: %{\n    ids: [\"acc_0000AZ2OJbCJNYH4Y2Zm5j\"],\n    fetch_rates: true\n  },\n  check_in_date: \"2026-08-01\",\n  check_out_date: \"2026-08-03\",\n  rooms: 1,\n  guests: [%{type: \"adult\"}]\n})\n```\n\n### Search Results\n\nFetch all rates for a search result:\n\n```elixir\n{:ok, response} = Travel.Stays.SearchResults.fetch_all_rates(config, \"ser_123\")\n```\n\n### Quotes\n\nCreate and retrieve quotes:\n\n```elixir\n# Create a quote from a rate\n{:ok, response} = Travel.Stays.Quotes.create(config, \"rate_123\")\n\n# Get a quote\n{:ok, response} = Travel.Stays.Quotes.get(config, \"quo_123\")\n```\n\n### Bookings\n\nCreate, retrieve, list, and cancel bookings:\n\n```elixir\n# Create a booking\n{:ok, response} = Travel.Stays.Bookings.create(config, %{\n  quote_id: \"quo_123\",\n  guests: [%{given_name: \"John\", family_name: \"Smith\"}],\n  email: \"john@example.com\",\n  phone_number: \"+447700900000\",\n  loyalty_programme_account_number: \"123456789\",  # optional\n  accommodation_special_requests: \"Late check-in\"  # optional\n})\n\n# Get a booking\n{:ok, response} = Travel.Stays.Bookings.get(config, \"bok_123\")\n\n# List bookings (paginated)\n{:ok, response} = Travel.Stays.Bookings.list(config, %{limit: 20})\n\n# Stream all bookings (handles pagination automatically)\nTravel.Stays.Bookings.stream(config)\n|\u003e Enum.each(fn response -\u003e\n  Enum.each(response.data, fn booking -\u003e\n    IO.puts(\"#{booking.id} - #{booking.status}\")\n  end)\nend)\n\n# Cancel a booking\n{:ok, response} = Travel.Stays.Bookings.cancel(config, \"bok_123\")\n```\n\n### Accommodation\n\n```elixir\n# Get by ID\n{:ok, response} = Travel.Stays.Accommodation.get(config, \"acc_123\")\n\n# List near a location\n{:ok, response} = Travel.Stays.Accommodation.list(config, %{\n  latitude: 51.5,\n  longitude: -0.1,\n  radius: 5\n})\n\n# Get suggestions\n{:ok, response} = Travel.Stays.Accommodation.suggestions(config, \"Hilton London\")\n\n# Get suggestions with location filter\n{:ok, response} = Travel.Stays.Accommodation.suggestions(config, \"Hilton\", %{\n  radius: 10,\n  geographic_coordinates: %{latitude: 51.5, longitude: -0.1}\n})\n\n# Get reviews\n{:ok, response} = Travel.Stays.Accommodation.reviews(config, \"acc_123\", %{limit: 10})\n```\n\n### Brands\n\n```elixir\n# List all brands\n{:ok, response} = Travel.Stays.Brands.list(config)\n\n# Get a brand\n{:ok, response} = Travel.Stays.Brands.get(config, \"brd_123\")\n```\n\n### Loyalty Programmes\n\n```elixir\n{:ok, response} = Travel.Stays.LoyaltyProgrammes.list(config)\n```\n\n## Flights API\n\n### Offer Requests\n\nCreate flight searches and retrieve offers:\n\n```elixir\n# Create and return offers\n{:ok, response} = Travel.Flights.OfferRequests.create(config, %{\n  slices: [\n    %{origin: \"LHR\", destination: \"JFK\", departure_date: \"2026-08-01\"},\n    %{origin: \"JFK\", destination: \"LHR\", departure_date: \"2026-08-15\"}\n  ],\n  passengers: [\n    %{type: \"adult\"},\n    %{type: \"child\", age: 8}\n  ],\n  cabin_class: \"economy\"\n}, %{return_offers: true})\n\n# Get an offer request\n{:ok, response} = Travel.Flights.OfferRequests.get(config, \"orq_123\")\n\n# List offer requests\n{:ok, response} = Travel.Flights.OfferRequests.list(config, %{limit: 20})\n```\n\n### Offers\n\n```elixir\n# List offers for an offer request\n{:ok, response} = Travel.Flights.Offers.list(config, \"orq_123\")\n\n# Get a specific offer\n{:ok, response} = Travel.Flights.Offers.get(config, \"off_123\", %{\n  return_available_services: true\n})\n\n# Update passenger details\n{:ok, response} = Travel.Flights.Offers.update(config, \"off_123\", \"pas_123\", %{\n  given_name: \"John\",\n  family_name: \"Smith\",\n  loyalty_programme_accounts: [%{account_number: \"123456\", airline_iata_code: \"BA\"}]\n})\n\n# Price an offer\n{:ok, response} = Travel.Flights.Offers.get_priced(config, \"off_123\", %{\n  intended_payment_methods: [%{type: \"balance\"}],\n  intended_services: []\n})\n```\n\n### Orders\n\n```elixir\n# Create an order\n{:ok, response} = Travel.Flights.Orders.create(config, %{\n  selected_offers: [\"off_123\"],\n  passengers: [\n    %{\n      given_name: \"John\",\n      family_name: \"Smith\",\n      born_on: \"1990-01-01\",\n      gender: \"m\",\n      title: \"mr\",\n      email: \"john@example.com\",\n      phone_number: \"+447700900000\"\n    }\n  ],\n  type: \"instant\",\n  metadata: %{\"customer_ref\" =\u003e \"ABC123\"}\n})\n\n# Get an order\n{:ok, response} = Travel.Flights.Orders.get(config, \"ord_123\")\n\n# List orders\n{:ok, response} = Travel.Flights.Orders.list(config, %{\n  awaiting_payment: true\n})\n\n# Update order metadata\n{:ok, response} = Travel.Flights.Orders.update(config, \"ord_123\", %{\n  metadata: %{\"payment_intent_id\" =\u003e \"pit_123\"}\n})\n\n# Get available services\n{:ok, response} = Travel.Flights.Orders.get_available_services(config, \"ord_123\")\n\n# Add services (baggage, seats)\n{:ok, response} = Travel.Flights.Orders.add_services(config, \"ord_123\", %{\n  payment: %{type: \"balance\", amount: \"30.00\", currency: \"GBP\"},\n  add_services: [%{id: \"asr_123\", quantity: 1}]\n})\n```\n\n### Payments\n\n```elixir\n# Pay for a pay-later order\n{:ok, response} = Travel.Flights.Payments.create(config, %{\n  order_id: \"ord_123\",\n  payment: %{type: \"balance\", amount: \"150.00\", currency: \"GBP\"}\n})\n```\n\n### Seat Maps\n\n```elixir\n{:ok, response} = Travel.Flights.SeatMaps.get(config, %{offer_id: \"off_123\"})\n```\n\n### Order Cancellations\n\n```elixir\n# Create a cancellation\n{:ok, response} = Travel.Flights.OrderCancellations.create(config, %{\n  order_id: \"ord_123\"\n})\n\n# Get a cancellation\n{:ok, response} = Travel.Flights.OrderCancellations.get(config, \"ore_123\")\n\n# List cancellations\n{:ok, response} = Travel.Flights.OrderCancellations.list(config, %{order_id: \"ord_123\"})\n\n# Confirm a cancellation\n{:ok, response} = Travel.Flights.OrderCancellations.confirm(config, \"ore_123\")\n```\n\n### Order Changes\n\n```elixir\n# Create a change request\n{:ok, response} = Travel.Flights.OrderChangeRequests.create(config, %{\n  order_id: \"ord_123\",\n  slices: %{\n    add: [%{origin: \"LHR\", destination: \"CDG\", departure_date: \"2026-09-01\"}],\n    remove: [%{slice_id: \"sli_123\"}]\n  }\n})\n\n# List change offers for a change request\n{:ok, response} = Travel.Flights.OrderChangeOffers.list(config, %{\n  order_change_request_id: \"ocr_123\"\n})\n\n# Create an order change\n{:ok, response} = Travel.Flights.OrderChanges.create(config, %{\n  selected_order_change_offer: \"oco_123\"\n})\n\n# Confirm the change (without payment)\n{:ok, response} = Travel.Flights.OrderChanges.confirm(config, \"orc_123\")\n\n# Confirm with payment\n{:ok, response} = Travel.Flights.OrderChanges.confirm(config, \"orc_123\", %{\n  payment: %{type: \"balance\", amount: \"50.00\", currency: \"GBP\"}\n})\n```\n\n### Batch Offer Requests\n\nFor long-polling searches that return offers incrementally:\n\n```elixir\n# Create\n{:ok, response} = Travel.Flights.BatchOfferRequests.create(config, %{\n  slices: [...],\n  passengers: [...]\n})\n\n# Poll for results\n{:ok, response} = Travel.Flights.BatchOfferRequests.get(config, \"bor_123\")\n```\n\n### Partial Offer Requests\n\nFor multi-step search flows:\n\n```elixir\n{:ok, response} = Travel.Flights.PartialOfferRequests.create(config, %{\n  slices: [...],\n  passengers: [...]\n})\n\n{:ok, response} = Travel.Flights.PartialOfferRequests.get(config, \"por_123\", %{\n  selected_partial_offer: \"off_123\"\n})\n\n# Get fares for a partial offer request\n{:ok, response} = Travel.Flights.PartialOfferRequests.get_fares_by_id(config, \"por_123\", %{\n  selected_partial_offer: \"off_456\"\n})\n```\n\n### Airline-Initiated Changes\n\n```elixir\n# List changes for an order\n{:ok, response} = Travel.Flights.AirlineInitiatedChanges.list(config, %{order_id: \"ord_123\"})\n\n# Accept a change\n{:ok, response} = Travel.Flights.AirlineInitiatedChanges.accept(config, \"aic_123\")\n\n# Update with action taken\n{:ok, response} = Travel.Flights.AirlineInitiatedChanges.update(config, \"aic_123\", %{\n  action_taken: \"accepted\"\n})\n```\n\n### Airline Credits\n\n```elixir\n# Create an airline credit\n{:ok, response} = Travel.Flights.AirlineCredits.create(config, %{\n  airline_iata_code: \"BA\",\n  amount: \"100.00\",\n  amount_currency: \"GBP\",\n  code: \"1234567890123\",\n  type: \"eticket\",\n  issued_on: \"2026-01-15\",\n  expires_at: \"2027-01-15T00:00:00Z\"\n})\n\n# Get an airline credit\n{:ok, response} = Travel.Flights.AirlineCredits.get(config, \"acd_123\")\n\n# List airline credits\n{:ok, response} = Travel.Flights.AirlineCredits.list(config, %{user_id: \"icu_123\"})\n```\n\n## Response Format\n\nAll functions return `{:ok, response} | {:error, error}` tuples.\n\n### Success Response\n\n```elixir\n{:ok, %Travel.Types.DuffelResponse{\n  data: %Travel.Stays.Types.StaysBooking{...},\n  meta: %Travel.Types.PaginationMeta{limit: 20, after: \"cursor_abc\", before: nil},\n  status: 200,\n  headers: %{\"x-request-id\" =\u003e [\"req_123\"]}\n}}\n```\n\n### Error Response\n\n```elixir\n{:error, %Travel.Error{\n  status: 400,\n  code: \"invalid_request\",\n  message: \"Field 'check_in_date' must be after today\",\n  title: \"Bad Request\",\n  type: \"validation_error\",\n  request_id: \"req_123\",\n  documentation_url: \"https://duffel.com/docs/api/errors\"\n}}\n```\n\n## Pagination\n\nList endpoints support cursor-based pagination:\n\n```elixir\n# Single page\n{:ok, response} = Travel.Stays.Bookings.list(config, %{limit: 20, after: \"cursor_abc\"})\n\n# Auto-paginating stream\nTravel.Stays.Bookings.stream(config)\n|\u003e Stream.flat_map(\u0026 \u00261.data)\n|\u003e Enum.to_list()\n```\n\n## Testing\n\n```bash\nmix test\n```\n\nAll tests use [Bypass](https://github.com/PSPDFKit-labs/bypass) for HTTP mocking — no network or API credentials required.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flambdachad%2Ftravel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flambdachad%2Ftravel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flambdachad%2Ftravel/lists"}