> ## 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.

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

<Note>
  The Methods app block already supports hourly booking when the shop has pick up / drop off time selection enabled. Build a custom widget only when you need bespoke UI that the app block can't provide.
</Note>

Everything runs against the Shopify App Proxy, so requests go to the shop's own domain and are signed by Shopify automatically. You never send an API key.

```
https://{shop_domain}/{proxy_path_prefix}/{path}
```

## Prerequisite: time selection must be enabled

Hourly booking is only available when both of these are true for the shop:

* The `pick_up_drop_off_time_selection` feature flag is enabled. Ask Supercycle to enable it.
* The pick up / drop off method has time selection turned on, with a window and interval configured in the Supercycle admin.

When time selection is off, treat the booking as day based (date only, no time).

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

    ```javascript 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 pick up / drop off entry carries:

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

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

    <ParamField path="timeIntervalMinutes" type="integer">
      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, not the per-day [Availability timeline](/api-reference/storefront/availability-timelines). It returns availability with time-of-day precision, which is what time slots need.

    ```
    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, offset-free timestamp (`YYYY-MM-DDTHH:MM:SS`); use it as-is and do not apply a timezone offset.

    The counts already account for prep and turnaround time. The merchant can set a **preparation time** (before an item can go back out: cleaning, checks) and a **restock time** (after it comes back) on the methods, and these can be expressed in hours. So an item returned at 14:00 with a 6-hour restock will not free up until 20:00, and a slot earlier that evening will show as unavailable. You don't compute any of this in the widget; just honour the `occupancy` counts.

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

  <Step title="Grey out unavailable dates and times">
    For a candidate pick up instant (chosen date and 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 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 entirely client-side.
  </Step>

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

    ```
    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">
      Pick up 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 for day based booking.
    </ParamField>

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

## See also

* [Availability log](/api-reference/storefront/availability-log) API reference
* [Create an intent](/api-reference/storefront/intent) API reference
* [Methods app block](/developers/app-blocks/methods)
