> ## Documentation Index
> Fetch the complete documentation index at: https://docs.supercycle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build an hourly booking widget

> Build a custom storefront widget that books rentals by date and time of day on the Storefront API

<Info>
  The Methods app block already supports hourly booking when pickup or drop-off time selection is on for your store. Build a custom widget only when you need a UI the block can't provide.
</Info>

This guide builds a custom storefront widget that books rentals by the hour, with pickup and drop-off at a chosen time of day, in place of the [Methods app block](/developers/app-blocks/methods). It follows the same data flow the block uses: read the store's time configuration, fetch availability, render date and time slots, then create an intent and add it to the cart.

Everything runs through the Shopify app proxy, so requests go to the store's own domain and Shopify signs them. You never send an API key.

```text Request shape theme={null}
https://{shop_domain}/{proxy_path_prefix}/{path}
```

***

## Prerequisites

Hourly booking needs two things to be true for the store:

* The `pick_up_drop_off_time_selection` feature flag is on. Contact support from the app to turn it on.
* The pickup or drop-off method has time selection turned on, with a window and an interval set under <Icon icon="shopify" iconType="solid" /> **[Logistics and locations](https://admin.shopify.com/apps/supercycle/settings/logistics)**.

When time selection is off, treat the booking as day based: a date with no time.

***

## Build the widget

<Steps>
  <Step title="Read the time window configuration">
    The store's logistics configuration is injected into the theme by the Supercycle Engine app embed, the same source the Methods block reads. There's no separate endpoint, so read it from the page:

    ```javascript Read the configuration theme={null}
    const { deliveryMethods, returnMethods, defaultDeliveryMethodType, defaultReturnMethodType, locations } =
      window.supercycleAppEmbed.context.appSettings;

    const pickUp  = deliveryMethods.find((m) => m.deliveryMethodType === "pick_up");
    const dropOff = returnMethods.find((m) => m.returnMethodType === "drop_off");
    ```

    Each pickup and drop-off entry carries:

    <ParamField path="allowTimeSelection" type="boolean">
      Whether this leg offers time slots. When `false`, render a date only.
    </ParamField>

    <ParamField path="fromTime / toTime" type="string">
      The daily window the slots span, as `"HH:MM"`, for example `"09:00"` to `"17:00"`.
    </ParamField>

    <ParamField path="timeIntervalMinutes" type="integer">
      The slot step in minutes: `15`, `30`, or `60`.
    </ParamField>

    Generate slots by stepping from `fromTime` to `toTime` in `timeIntervalMinutes` increments. `defaultDeliveryMethodType` and `defaultReturnMethodType` tell you which legs to preselect.
  </Step>

  <Step title="Fetch availability">
    Use the [Availability log](/api-reference/storefront/availability-log) endpoint rather than the per-day [Availability timeline](/api-reference/storefront/availability-timelines). It returns availability with time-of-day precision, which is what time slots need.

    ```text Availability log request theme={null}
    GET https://{shop_domain}/{proxy_path_prefix}/availability_log
          ?variant_shopify_id=44556677
          &delivery_method_type=pick_up
          &return_method_type=drop_off
          &location_id=12345
    ```

    The `occupancy` array is a list of change points, not one entry per day. Each `{ at, available }` entry means that from `at` onward, `available` items are free until the next entry. `at` is a shop-local timestamp with no offset (`YYYY-MM-DDTHH:MM:SS`). Use it as it is and don't apply a time zone offset.

    The counts already include preparation and restock time. You can set a preparation time (before an item goes out again, for cleaning and checks) and a restock time (after it comes back) on the methods, in hours. An item returned at 2 pm with a 6-hour restock doesn't free up until 8 pm, so a slot earlier that evening shows as unavailable. The widget doesn't compute any of this. It honors the `occupancy` counts.

    <Note>
      Availability is expensive to compute and is cached, so fetch it again only when the selected variant, delivery or return method, or location changes.
    </Note>
  </Step>

  <Step title="Gray out unavailable dates and times">
    For a candidate pickup instant (a date plus a slot time), the number of items available is the `available` value of the last `occupancy` entry whose `at` is at or before it. A slot is bookable only if items stay available across the whole window the customer is requesting.

    ```javascript Is a slot bookable theme={null}
    // occupancy: sorted ascending by `at` (treat as shop-local wall-clock)
    function slotBookable(occupancy, startISO, endISO) {
      let level = 0;
      for (const { at, available } of occupancy) {
        if (at <= startISO) level = available;       // level entering the window
      }
      let min = level;
      for (const { at, available } of occupancy) {
        if (at > startISO && at < endISO) min = Math.min(min, available); // changes inside it
      }
      return min > 0;
    }
    ```

    Disable any date or time whose window returns `false`. Because the log is sparse, this is cheap to evaluate on the client.
  </Step>

  <Step title="Create the intent and add to cart">
    When the customer has chosen their window, create the intent with the [Intent](/api-reference/storefront/intent) endpoint, passing the times of day alongside the date:

    ```text Create an intent theme={null}
    POST https://{shop_domain}/{proxy_path_prefix}/intents
    Content-Type: application/json

    {
      "variant_id": 44556677,
      "option": {
        "global_id": "gid://supercycle/CalendarRental::RentalPeriod/1",
        "params": {
          "rental_start": "2026-06-27",
          "arrive_by_time": "14:30",
          "return_by_time": "17:00",
          "delivery_method_type": "pick_up",
          "return_method_type": "drop_off",
          "location_id": 12345
        }
      }
    }
    ```

    <ParamField path="option.params.arrive_by_time" type="string">
      Pickup time of day (`"HH:MM"`) from your slot picker. This is what makes the booking hourly. Omit it when the leg's `allowTimeSelection` is `false` to fall back to day-based booking.
    </ParamField>

    <ParamField path="option.params.return_by_time" type="string">
      Drop-off time of day (`"HH:MM"`). Omit it for day-based booking.
    </ParamField>

    The response contains an `attributes` object. Pass it straight through as the cart line's attributes when you add the variant to the cart. It carries the line item properties and the `selling_plan`. On a problem (variant or option not found, or the method not turned on) the endpoint returns `422` with `{ "error": "..." }`. Show the message to the customer and block add to cart.
  </Step>
</Steps>

***

## Related documentation

<CardGroup cols={2}>
  <Card title="Availability log" icon="clock" href="/api-reference/storefront/availability-log">
    Fetch a variant's availability with time-of-day precision.
  </Card>

  <Card title="Intent" icon="cart-plus" href="/api-reference/storefront/intent">
    Create the intent that turns a cart line into a cycle.
  </Card>

  <Card title="Methods" icon="calendar-days" href="/developers/app-blocks/methods">
    Use the app block that already supports hourly booking.
  </Card>
</CardGroup>
