npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@awayz/web-components

v0.1.2

Published

Framework agnostic headless web components for embedding Awayz travel search and booking into your own webapps.

Readme

Awayz Web Components

A collection of framework agnostic headless web components built with Lit for developers who want to embed Awayz into their own travel webapps. The components handle authentication, API calls and state; you keep complete control over markup and styling. First-class React wrappers ship alongside the custom elements.

npm install @awayz/web-components

Importing the package registers the custom elements as a side effect:

import '@awayz/web-components';

In a React app, import the wrapper components instead (React is an optional peer dependency, >=18):

import {
  AwayzProvider,
  AwayzHotelContext,
  AwayzHotelDetails,
  AwayzHotelRooms,
  AwayzLocationSearch,
  AwayzHotelResults,
  AwayzHotelBooking,
  AwayzCardCapture
} from '@awayz/web-components';

Quick start

<awayz-provider client-id="..." id-token="..." refresh-token="...">
  <awayz-hotel-context
    check-in-date="2026-07-01"
    check-out-date="2026-07-05"
    guests="2"
    rooms="1"
    hotel-groups="HYATT,MARRIOTT,HILTON"
    auto-search
  >
    <awayz-location-search>
      <input type="search" placeholder="Where are you going?" />
      <template>
        <div class="suggestion">{{description}}</div>
      </template>
    </awayz-location-search>

    <awayz-hotel-results>
      <p slot="loading">Searching…</p>
      <p slot="empty">No hotels found.</p>
      <template>
        <article class="card">
          <img src="{{images.0}}" alt="{{name}}" />
          <h3>{{name}}</h3>
          <p>{{city}}, {{country}} — {{cash_label}}</p>
          <a href="{{booking_link}}">Book</a>
        </article>
      </template>
    </awayz-hotel-results>
  </awayz-hotel-context>
</awayz-provider>

React

Each component is also exported as a React wrapper (built with @lit/react). The wrappers accept the same options as camelCased props, expose the custom events as on* callbacks, and render the same headless <template> children.

function HotelSearch() {
  return (
    <AwayzProvider
      clientId='...'
      idToken='...'
      refreshToken='...'
      onAuthenticationSuccess={(e) => console.log(e.detail.user)}
    >
      <AwayzHotelContext
        checkInDate='2026-07-01'
        checkOutDate='2026-07-05'
        guests={2}
        rooms={1}
        hotelGroups={['HYATT', 'MARRIOTT', 'HILTON']}
        autoSearch
        onHotelsResults={(e) => console.log(e.detail)}
      >
        <AwayzLocationSearch
          onLocationSelect={(e) => console.log(e.detail.suggestion)}
        >
          <input type='search' placeholder='Where are you going?' />
          <template>
            <div className='suggestion'>{'{{description}}'}</div>
          </template>
        </AwayzLocationSearch>

        <AwayzHotelResults onHotelsSelect={(e) => console.log(e.detail)}>
          <p slot='loading'>Searching…</p>
          <p slot='empty'>No hotels found.</p>
          <template>
            <article className='card'>
              <img src='{{images.0}}' alt='{{name}}' />
              <h3>{'{{name}}'}</h3>
              <p>{'{{city}}, {{country}}'}</p>
              <a href='{{booking_link}}'>Book</a>
            </article>
          </template>
        </AwayzHotelResults>
      </AwayzHotelContext>
    </AwayzProvider>
  );
}

Notes for React:

  • Props that map to comma-separated attributes (hotelGroups) are set as element properties, so pass them as arrays rather than strings.
  • {{dot.path}} bindings work in quoted attribute values as-is (src="{{images.0}}"), but in JSX text nodes wrap them in a string so JSX doesn't treat the braces as an expression: {'{{name}}'}.
  • event.detail carries the same payload documented for each component's events below.

Template bindings

Headless rendering uses plain <template> children. {{dot.path}} placeholders in text and attributes are replaced with values from the current item (a hotel result or a location suggestion). Arrays are joined with ", ", and indexes are supported ({{images.0}}). Missing values render as empty strings. Templates stamp correctly whether the browser's HTML parser populated them or a virtual-DOM library (React) built them with createElement.

Components

<awayz-provider>

Wraps all other components. Holds credentials, exchanges them for an access token and provides an authenticated axios instance plus a TanStack Query client (@tanstack/query-core) to descendants via context. Every API call made through it — including all hotel calls — carries the client-id header and, once authenticated, Authorization: Bearer <accessToken>.

| Attribute | Description | | --------------- | ----------------------------------------------------------------------------------------------------- | | client-id | Awayz client id (also sent as the client-id header on every request) | | id-token | OIDC id token | | refresh-token | Refresh token | | test-mode | Boolean. When present, requests go to https://appdev.odynn.info; otherwise https://app.odynn.info |

When client-id, id-token and refresh-token are all populated the provider calls POST /user/exchange-token. Any later API request that receives a 401 triggers a new exchange with the stored refresh token and a single retry (handled by an axios response interceptor).

Descendant components (and your own code, via the awayzContext @lit/context) get access to:

  • api — the authenticated AxiosInstance
  • queryClient — a shared QueryClient; hotel searches are cached for 60 s and city lookups for 5 min, with concurrent identical requests deduplicated

Events:

  • awayz:authentication:successdetail: { user, accessToken }
  • awayz:authentication:faileddetail: { error }

<awayz-hotel-context>

Holds all parameters for a hotel search, executes POST /hotels/search-by-coordinates, and shares parameters + results with descendant components.

| Attribute | Default | Maps to | | -------------------- | ------- | ------------------------------------------------------------------- | | type | cash | cash_or_pointscash or points (selects the pricing dimension this context searches) | | check-in-date | — | check_in_date | | check-out-date | — | check_out_date | | guests | 2 | guests | | rooms | 1 | rooms | | lat / lon | — | lat / lon | | search-range | 10 | search_range | | range-type | km | range_type | | hotel-groups | major chains | hotel_groups (comma separated). Defaults to the major chains: marriott, choice, hilton, ihg, hyatt, accor, wyndham | | location-type | city | type | | external-inventory | false | external_inventory | | auto-search | false | run a search automatically whenever all required params are present |

use_duffel is no longer an attribute — it is derived from type: true for a cash context, false for a points context.

region_code is no longer an attribute — it is taken from the authenticated user's region (user.user_region), falling back to US.

Results from both type contexts are normalized to the same HotelResult shape. For cash results backed by Duffel inventory, the accommodation_id becomes the hotel_id and the cash_booking_offers object supplies the latest pricing, images, review score and description.

Methods / events:

  • search(): Promise<HotelResult[]> — runs the search imperatively
  • setLocation({ lat, lon, type }) — used by <awayz-location-search> on selection
  • awayz:hotels:loading / awayz:hotels:results / awayz:hotels:error

<awayz-location-search>

Headless autocomplete backed by GET /hotels/search/cities?search=…. Place an <input> inside it; typing is debounced (debounce, default 300 ms, after min-length characters, default 2) and suggestions are stamped from your <template> into a [data-awayz-suggestions] element (auto-created if omitted). Clicking a suggestion fills the input, pushes lat/lon/type into the surrounding <awayz-hotel-context> and emits awayz:location:select.

Events: awayz:location:results, awayz:location:select, awayz:location:error.

<awayz-hotel-results>

Headless renderer for the results held by the surrounding <awayz-hotel-context>. Your <template> is stamped once per hotel into a [data-awayz-results] element (auto-created if omitted). The current state is reflected as state="idle|loading|ready|empty|error" for CSS hooks, and slot="loading" / slot="empty" / slot="error" children are shown for the matching state. Clicking a stamped item emits awayz:hotels:select with the hotel in detail.

| Attribute | Default | Description | | --------- | ------- | ------------------------------------------------------------------------------------------------- | | type | cash | Which pricing context to read — cash or points. Binds this element to the matching <awayz-hotel-context type="…">. |

Pairing two <awayz-hotel-context> elements (one type="cash", one type="points") with two <awayz-hotel-results type="…"> elements lets you render cash and points results side-by-side without either context shadowing the other.

Derived pricing fields

The backend returns -1 for a cash amount or points cost to mean "no availability for this option". To keep that sentinel out of your markup, each result template is stamped with extra computed bindings alongside the raw HotelResult fields:

| Binding | Example | Description | | ------------------------ | ---------------- | -------------------------------------------------------------------------------------------- | | {{cash_label}} | USD 1234 | Cash price, N/A when cash is unavailable, or "" when neither option is available | | {{points_label}} | 45000 pts | Points price, N/A when points are unavailable, or "" when neither option is available | | {{availability_state}} | both | One of none / cash / points / both — handy on a data-* attribute to drive CSS | | {{availability_label}} | No availability| No availability when neither option is bookable, otherwise "" |

<template>
  <article class="card" data-availability="{{availability_state}}">
    <h3>{{name}}</h3>
    <p class="cash">{{cash_label}}</p>
    <p class="points">{{points_label}}</p>
    <p class="sold-out">{{availability_label}}</p>
  </article>
</template>

Results can be filtered and sorted client-side without re-querying, both via host-supplied callbacks set as properties (callbacks can't be expressed as HTML attributes; React: pass them as props).

<awayz-hotel-details>

Fetches and renders the full detail page for a single hotel. Calls different endpoints depending on type and normalises both responses into the same HotelDetails shape, so your templates and event handlers work identically for cash and points.

| Attribute | Required | Description | | ---------------- | -------- | ---------------------------------------------------------------------------------------------- | | hotel-id | Yes | Duffel accommodation ID (cash) or awayz hotel_id (points) | | hotel-group | Yes | Hotel group / programme (e.g. hyatt). Required by the points endpoint. | | type | Yes | cash or points — selects the pricing endpoint | | check-in-date | No† | Overrides the date from an enclosing <awayz-hotel-context> | | check-out-date | No† | Overrides the date from an enclosing <awayz-hotel-context> | | rooms | No | Number of rooms (default 1); overrides context | | guests | No | Number of guests (default 2); overrides context | | region-code | No | ISO country code for pricing (default: user region → US); overrides context | | auto-fetch | No | Boolean. Fetches automatically when all required params are present and authentication is ready |

† Required when the component is not nested inside a <awayz-hotel-context>.

Endpoint mapping:

| type | Endpoint | Identifier used | | -------- | ------------------------------------- | ----------------- | | cash | POST /duffel/hotels/search-accommodation | hotel-id as Duffel accommodation_id | | points | POST /hotels/search-by-id | hotel-id as awayz hotel_id |

State is reflected on state="idle|loading|ready|error" for CSS hooks.

Events:

  • awayz:hotel-details:loading — fetch started
  • awayz:hotel-details:readydetail: { details: HotelDetails }
  • awayz:hotel-details:errordetail: { error: string }

fetch(): Promise<HotelDetails> — call programmatically to trigger a fetch.

Template contract

Main template (<template> with no attributes) — stamped once into a [data-awayz-container] child with all HotelDetails fields as {{dot.path}} bindings:

| Binding | Source | Notes | | ------------------------- | ---------- | --------------------------------------------------------------- | | {{name}} | both | | | {{description}} | both | | | {{address}} | both | | | {{city}} / {{country_code}} | both | | | {{telephone}} | both | | | {{rating}} | both | Star rating (1–5) | | {{review_score}} | both | User score — scale varies by source | | {{review_count}} | both | | | {{images.0}} | both | First image URL | | {{amenities}} | both | Joined as ", " | | {{check_in_date}} / {{check_out_date}} | both | | | {{check_in_time}} / {{check_out_time}} | cash | null for points | | {{booking_link_cash}} / {{booking_link_points}} | points | "" for cash | | {{award_points}} | points | null for cash | | {{award_category}} | points | null for cash | | {{cheapest_rate_amount}} / {{cheapest_rate_currency}} | cash | null for points | | {{hotel_program.program}} / {{hotel_program.logo}} | points | null for cash | | {{brand.name}} / {{chain.name}} | cash | null for points |

Rooms are rendered by a nested <awayz-hotel-rooms> component — see below.

Each room's rates array is available on the HotelDetails object via the awayz:hotel-details:ready event. Every rate has a category field — 'cash', 'points', or 'points_cash' — so consumers never need to branch on type.

HTML example

<awayz-hotel-details
  hotel-id="vieph"
  hotel-group="hyatt"
  type="points"
  check-in-date="2026-07-21"
  check-out-date="2026-07-22"
  auto-fetch
>
  <p slot="loading">Loading…</p>
  <p slot="error">Failed to load hotel.</p>

  <template>
    <div class="hotel">
      <img src="{{images.0}}" alt="{{name}}" />
      <h1>{{name}}</h1>
      <p>{{description}}</p>
      <p>{{award_points}} pts · Category {{award_category}}</p>
    </div>
  </template>

  <awayz-hotel-rooms>
    <p slot="empty">No rooms available.</p>
    <template>
      <article class="room">
        <img src="{{image}}" alt="{{name}}" />
        <h3>{{name}}</h3>
        <p>From {{lowest_points}} pts</p>
      </article>
    </template>
  </awayz-hotel-rooms>
</awayz-hotel-details>

React example

<AwayzHotelDetails
  hotelId="vieph"
  hotelGroup="hyatt"
  type="points"
  checkInDate="2026-07-21"
  checkOutDate="2026-07-22"
  autoFetch
  onHotelDetailsReady={(e) => console.log(e.detail.details)}
  onHotelDetailsError={(e) => console.error(e.detail.error)}
>
  <p slot="loading">Loading…</p>
  <template>
    <div className="hotel">
      <h1>{'{{name}}'}</h1>
      <p>{'{{award_points}}'} pts · Category {'{{award_category}}'}</p>
    </div>
  </template>

  <AwayzHotelRooms onRoomSelect={(e) => console.log(e.detail.room)}>
    <p slot="empty">No rooms available.</p>
    <template>
      <article className="room">
        <h3>{'{{name}}'}</h3>
        <p>{'{{lowest_points}}'} pts</p>
      </article>
    </template>
  </AwayzHotelRooms>
</AwayzHotelDetails>

Nested inside <awayz-hotel-context>

When nested, check-in-date, check-out-date, rooms, guests and region-code are automatically inherited from the surrounding context. Provide hotel-id, hotel-group and type — typically from a awayz:hotels:select event — and call fetch() (or set auto-fetch):

document.querySelector('awayz-hotel-results').addEventListener('awayz:hotels:select', (e) => {
  const details = document.querySelector('awayz-hotel-details');
  details.setAttribute('hotel-id', e.detail.hotel.hotel_id);
  details.setAttribute('hotel-group', e.detail.hotel.hotel_group);
  details.fetch();
});

<awayz-hotel-rooms>

Headless renderer for the rooms inside the surrounding <awayz-hotel-details>. Works identically to <awayz-hotel-results> but consumes the hotel details context instead of a search context. Must be nested inside <awayz-hotel-details>.

Your <template> is stamped once per room into a [data-awayz-rooms] element (auto-created if omitted). The current state is reflected as state="idle|loading|ready|empty|error" for CSS hooks, and slot="loading" / slot="empty" / slot="error" children are shown for the matching state. Clicking a stamped item emits awayz:hotel-details:room:select with the room in detail.

Room template bindings

| Binding | Source | Notes | | --------------------- | ------ | --------------------------------------------------- | | {{name}} | both | Room name | | {{description}} | both | null for cash | | {{image}} | both | First room photo URL | | {{type}} | points | null for cash | | {{category}} | points | null for cash | | {{lowest_points}} | points | null for cash | | {{lowest_cash_amount}} / {{lowest_cash_currency}} | cash | null for points | | {{beds}} | cash | Array joined as ", "; [] for points |

Filtering and sorting

Set filter and sort as properties (not attributes). They mirror Array.prototype.filter and Array.prototype.sort respectively and receive full HotelDetailsRoom objects. Both callbacks are typed as HotelRoomFilter and HotelRoomSort.

const rooms = document.querySelector('awayz-hotel-rooms');
rooms.filter = (room) => (room.lowest_points ?? 0) <= 50000;
rooms.sort = (a, b) => (a.lowest_points ?? 0) - (b.lowest_points ?? 0);
<AwayzHotelRooms
  filter={(room) => room.lowest_points !== null}
  sort={(a, b) => (a.lowest_cash_amount ?? 0) - (b.lowest_cash_amount ?? 0)}
  onRoomSelect={(e) => console.log(e.detail.room)}
>
  <template>…</template>
</AwayzHotelRooms>

<awayz-hotel-room-rates>

Headless renderer for the rates of the room the user just selected. When a room is clicked in <awayz-hotel-rooms>, its rates are automatically made available to this component via context — no manual wiring required. Place it anywhere inside <awayz-hotel-details>.

Your <template> is stamped once per rate into a [data-awayz-rates] element (auto-created if omitted). State is reflected as state="idle|ready|empty" for CSS hooks — idle means no room has been selected yet. A slot="empty" child is shown for both idle and empty states. Clicking a stamped rate emits awayz:hotel-details:rate:select with the rate in detail.

Rate template bindings

| Binding | Type | Notes | | -------------------------- | ----------------- | --------------------------------------------------- | | {{category}} | cash | points | points_cash | Always present | | {{name}} | string | null | | | {{description}} | string | null | | | {{points}} | number | null | Populated for points and points_cash rates | | {{total_amount}} | number | null | Populated for cash rates | | {{total_currency}} | string | null | | | {{base_amount}} | number | null | | | {{base_currency}} | string | null | | | {{board_type}} | string | null | e.g. room_only, breakfast | | {{payment_type}} | string | null | e.g. guarantee, deposit | | {{points_cash_amount}} | number | null | Cash portion for points_cash rates | | {{points_cash_currency}} | string | null | | | {{expires_at}} | string | null | ISO timestamp | | {{cancellation_timeline}}| array | Joined as ", " | | {{conditions}} | array | Joined as ", " |

Filtering and sorting

Set filter and sort as properties. Typed as HotelRateFilter and HotelRateSort.

HTML example

<awayz-hotel-details hotel-id="vieph" hotel-group="hyatt" type="points" auto-fetch>

  <awayz-hotel-rooms>
    <p slot="empty">No rooms available.</p>
    <template>
      <article class="room">
        <h3>{{name}}</h3>
        <p>From {{lowest_points}} pts</p>
      </article>
    </template>
  </awayz-hotel-rooms>

  <awayz-hotel-room-rates>
    <p slot="empty">Select a room to see rates.</p>
    <template>
      <div class="rate" data-category="{{category}}">
        <strong>{{name}}</strong>
        <span>{{total_amount}} {{total_currency}}</span>
        <span>{{points}} pts</span>
      </div>
    </template>
  </awayz-hotel-room-rates>

</awayz-hotel-details>

React example

<AwayzHotelDetails hotelId="vieph" hotelGroup="hyatt" type="points" autoFetch>

  <AwayzHotelRooms onRoomSelect={(e) => console.log('room', e.detail.room)}>
    <p slot="empty">No rooms available.</p>
    <template>
      <article className="room">
        <h3>{'{{name}}'}</h3>
        <p>{'{{lowest_points}}'} pts</p>
      </article>
    </template>
  </AwayzHotelRooms>

  <AwayzHotelRoomRates onRateSelect={(e) => console.log('rate', e.detail.rate)}>
    <p slot="empty">Select a room to see rates.</p>
    <template>
      <div className="rate">
        <strong>{'{{name}}'}</strong>
        <span>{'{{total_amount}}'} {'{{total_currency}}'}</span>
      </div>
    </template>
  </AwayzHotelRoomRates>

</AwayzHotelDetails>

Filtering

Set the filter property. It mirrors Array.prototype.filter — return true to keep a hotel — and receives the full HotelResult so you can filter on anything in it:

const results = document.querySelector('awayz-hotel-results');
// Only hotels with availability, under a price ceiling.
results.filter = (hotel) =>
  hotel.cash_available && hotel.cash_value.amount <= 300;
<AwayzHotelResults
  filter={(hotel, index, results) => hotel.review_rating !== null}
>
  {/* template … */}
</AwayzHotelResults>

The filter signature is (hotel: HotelResult, index: number, results: HotelResult[]) => boolean, exported as the HotelFilter type. It runs before the sort, and index / results refer to the unfiltered results in API order. Reassign the property to re-filter; clear it (set to undefined) to render everything again. When the filter removes every hotel the host reflects state="empty" and shows your slot="empty". A callback that throws is caught and logged, leaving the results unfiltered so a single bad call can't wedge rendering.

Sorting

Set the sort property. It mirrors Array.prototype.sort's compare function — return a negative number to place a before b, a positive number for after, or 0 to keep their order — and receives two full HotelResult objects so you can order on any field:

const results = document.querySelector('awayz-hotel-results');
// Cheapest cash price first.
results.sort = (a, b) => a.cash_value.amount - b.cash_value.amount;
<AwayzHotelResults sort={(a, b) => b.points - a.points}>
  {/* template … */}
</AwayzHotelResults>

The sort signature is (a: HotelResult, b: HotelResult) => number, exported as the HotelSort type. It runs after the filter, on the filtered results. Reassign the property to re-sort; clear it (set to undefined) to restore the order returned by the API. A comparator that throws is caught and logged, leaving the results unsorted so a single bad call can't wedge rendering.

<awayz-card-capture>

Hosts Evervault's PCI-compliant card iframe — the one piece of UI this library renders itself, because raw card details must never touch the host page. The iframe mounts into a light-DOM child marked data-awayz-card (auto-created if omitted); everything around it stays yours to build and style.

<awayz-card-capture>
  <p slot="loading">Loading card form…</p>
  <p slot="error">The card form failed to load.</p>
  <div data-awayz-card></div>
</awayz-card-capture>

Evervault credentials resolve automatically from GET /clients/embed-data once the ancestor <awayz-provider> authenticates, or set them explicitly with the team-id and app-id attributes (no provider needed in that case).

| Attribute / property | Type | Notes | | -------------------- | ----------------- | ---------------------------------------------------------------- | | team-id / app-id | string | Optional overrides; skip the embed-data lookup | | options | AwayzCardOptions (property) | Forwarded verbatim to Evervault's ui.card(options)theme, fields, translations, acceptedBrands, … Changing it calls card.update() |

The iframe's inner styling is customised through options.theme (any Evervault theme definition). State is reflected as state="idle|loading|ready|error".

| Event | detail | | --------------------- | ------------------------------------------- | | awayz:card:ready | {} — iframe mounted | | awayz:card:change | AwayzCardData — fires on every keystroke; card.number/card.cvc are already-encrypted Evervault tokens, plus lastFour, brand, bin, isComplete, isValid, errors | | awayz:card:complete | AwayzCardData — all fields valid | | awayz:card:error | { error } |

Methods: mount(), unmount(), and reset() (clears the fields by remounting a fresh iframe — call it after a failed payment). The latest payload is also available as the card property.

Use detail.isComplete (together with your own billing-address validation) to enable your pay button.

<awayz-hotel-booking>

Headless orchestrator for booking a hotel rate with a card ("cash" checkout). It runs the full production flow:

  1. QuotecreateQuote() (or the auto-quote attribute) locks the price of rate-id via POST /duffel/hotels/quote.
  2. Paypay() prepares the payment (POST /v2/payment/prepare) from the encrypted card + billing address, runs the provider's 3-D Secure challenge when required (via @duffel/components, loaded lazily), and completes the booking (POST /v2/hotel/booking).

The final booking call is never retried — a blind retry after a card charge could double-book. Repeated pay() calls while one is in flight return the same promise, so a double-clicked button can't double-book either.

| Attribute / property | Type | Notes | | --------------------- | ----------------------------------- | ----------------------------------------------------------- | | rate-id | string | The rate to quote — take it from awayz:hotel-details:rate:select | | auto-quote | boolean attribute | Quote automatically once authenticated and rate-id is set (re-quotes when rate-id changes) | | email | string | Contact email for the booking | | phone-number | string | Contact phone number | | guests | HotelBookingGuest[] (property) | [{ given_name, family_name }, …], main guest first | | billingAddress | BillingAddress (property) | Cardholder name + address from your own form | | card | AwayzCardData (property) | Auto-captured from a nested <awayz-card-capture>; set manually if the card form lives elsewhere | | verificationHandler | VerificationHandler (property) | Optional replacement for the built-in Duffel 3-D Secure step |

All of card, billingAddress, guests, email and phoneNumber can also be passed per call: pay({ guests, email, phoneNumber, billingAddress, card }). If no quote exists yet, pay() quotes rate-id first.

State is reflected as state="idle|quoting|quoted|preparing|verifying|booking|booked|error". Children with slot="quoting", slot="processing" (covers preparing/verifying/booking), slot="error" and slot="success" show for the matching states.

Templates

  • <template> (no slot) is stamped with the quote into [data-awayz-container] when the quote is ready. Bindings: {{total_amount}}, {{total_currency}}, {{base_amount}}, {{tax_amount}}, {{fee_amount}}, {{due_at_accommodation_amount}}, {{check_in_date}}, {{check_out_date}}, {{number_of_guests}}, {{number_of_rooms}}, {{accommodation.name}}, {{accommodation.image}}, {{accommodation.address}}, {{accommodation.rooms.0.name}}, …
  • <template data-awayz-confirmation> is stamped with the confirmed booking into [data-awayz-confirmation-container] on success. Bindings: {{awayzRef}}, {{providerRef}}, {{status}}, {{booking.reference}}, {{booking.totalAmount}}, {{booking.totalCurrency}}, {{booking.checkInDate}}, …

Events

| Event | detail | | ------------------------------------ | --------------------------------------------------------------- | | awayz:hotel-booking:quote:loading | { rate_id } | | awayz:hotel-booking:quote:ready | { quote: HotelBookingQuote } | | awayz:hotel-booking:preparing | { quote } — payment prepare started | | awayz:hotel-booking:verifying | { action, provider }action is 3ds_challenge or none | | awayz:hotel-booking:submitting | { paymentSessionId } — final booking call started | | awayz:hotel-booking:success | { booking: HotelBookingConfirmation } | | awayz:hotel-booking:error | { stage, error, errorSource?, errorCode?, awayzRef?, providerRef? }stage is quote/prepare/verify/book; awayzRef is the support reference to show travellers |

HTML example

<awayz-provider client-id="..." id-token="..." refresh-token="...">
  <awayz-hotel-booking id="booking" auto-quote>
    <p slot="quoting">Locking in your price…</p>
    <p slot="processing">Processing payment — don't close this page.</p>
    <div slot="error" class="error"></div>

    <template>
      <h2>{{accommodation.name}}</h2>
      <p>{{check_in_date}} → {{check_out_date}}</p>
      <p class="price">{{total_amount}} {{total_currency}}</p>
    </template>

    <awayz-card-capture></awayz-card-capture>

    <template data-awayz-confirmation>
      <h2>Booking confirmed 🎉</h2>
      <p>Reference: {{awayzRef}}</p>
    </template>
  </awayz-hotel-booking>
</awayz-provider>

<script type="module">
  const booking = document.querySelector('#booking');

  // 1. Wire the selected rate into the booking (from your details page).
  document.addEventListener('awayz:hotel-details:rate:select', (e) => {
    booking.rateId = e.detail.rate.id; // auto-quote re-quotes on change
  });

  // 2. Enable your pay button when the card is complete.
  booking.addEventListener('awayz:card:change', (e) => {
    payButton.disabled = !e.detail.isComplete;
  });

  // 3. Collect guests + billing from your own forms and pay.
  payButton.addEventListener('click', () => {
    booking.pay({
      guests: [{ given_name: 'Ada', family_name: 'Lovelace' }],
      email: '[email protected]',
      phoneNumber: '+15550001111',
      billingAddress: {
        name: 'Ada Lovelace',
        addressLine1: '1 Main St',
        addressCity: 'Austin',
        addressRegion: 'TX',
        addressPostalCode: '78701',
        addressCountryCode: 'US'
      }
    });
  });

  booking.addEventListener('awayz:hotel-booking:error', (e) => {
    // e.detail.stage tells you where it failed; card fields must be
    // re-entered after a payment failure:
    document.querySelector('awayz-card-capture').reset();
  });
</script>

React example

function Checkout({ rateId }) {
  const bookingRef = useRef(null);
  const [cardComplete, setCardComplete] = useState(false);

  return (
    <AwayzHotelBooking
      ref={bookingRef}
      rateId={rateId}
      autoQuote
      onQuoteReady={(e) => console.log('total', e.detail.quote.total_amount)}
      onBookingSuccess={(e) => navigate(`/confirmed/${e.detail.booking.awayzRef}`)}
      onBookingError={(e) => setError(e.detail)}
    >
      <template>
        <p>{'{{accommodation.name}}'} — {'{{total_amount}}'} {'{{total_currency}}'}</p>
      </template>

      <AwayzCardCapture
        onCardChange={(e) => setCardComplete(e.detail.isComplete)}
      />

      <button
        disabled={!cardComplete}
        onClick={() =>
          bookingRef.current.pay({
            guests: [{ given_name: 'Ada', family_name: 'Lovelace' }],
            email: '[email protected]',
            phoneNumber: '+15550001111',
            billingAddress: { /* from your billing form */ }
          })
        }
      >
        Pay now
      </button>
    </AwayzHotelBooking>
  );
}

3-D Secure

When POST /v2/payment/prepare answers action: "3ds_challenge", the built-in handler runs Duffel's in-browser challenge (createThreeDSecureSession from @duffel/components, imported on demand) against the quote and attaches the resulting threeDSecureSessionId to the booking. Replace it by setting the verificationHandler property — it receives { provider, action, providerData, bookingId, services } and must resolve with { provider, data }.