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

# Offers widget

> Build a cashback offers widget with the Cashback Hive SDK: list, search and paginate offers, show logos and rates, and link out through tracked URLs.

The Offers widget shows the cashback offers available to your property and sends shoppers to the retailer through a tracked link when they click.

## What it does

* Fetches offers with [`offers.list()`](https://developer.cashbackhive.com/reference/offers#offers-list), optionally filtered by search text or category.
* Renders each offer's name, logo, category and cashback rate.
* On click, calls [`openOffer()`](https://developer.cashbackhive.com/reference/tracking#openoffer) which opens a tracked link in a new tab and records the click.

## Vanilla JavaScript

```html theme={null}
<section id="cbh-offers">
  <input id="cbh-offers-search" type="search" placeholder="Search brands" />
  <ul id="cbh-offers-list"></ul>
</section>

<script>
  (function () {
    function formatCashback(offer) {
      if (!offer.cashback) return "No cashback";
      if (offer.cashback.type === "percentage") return offer.cashback.amount + "% back";
      return new Intl.NumberFormat(undefined, {
        style: "currency",
        currency: offer.cashback.currency || "USD",
      }).format(offer.cashback.amount) + " back";
    }

    function mount(sdk) {
      var list = document.getElementById("cbh-offers-list");
      var search = document.getElementById("cbh-offers-search");
      var requestId = 0;

      async function render(query) {
        var id = ++requestId;
        list.textContent = "Loading…";
        try {
          var result = await sdk.offers.list({ search: query, limit: 20 });
          if (id !== requestId) return; // a newer request superseded this one
          list.textContent = "";
          if (result.offers.length === 0) {
            list.textContent = "No offers found.";
            return;
          }
          result.offers.forEach(function (offer) {
            var li = document.createElement("li");

            if (offer.logoUrl) {
              var img = document.createElement("img");
              img.src = offer.logoUrl;
              img.alt = "";
              img.width = 40;
              li.append(img);
            }

            var name = document.createElement("strong");
            name.textContent = offer.name;

            var rate = document.createElement("span");
            rate.textContent = " " + formatCashback(offer);

            var btn = document.createElement("button");
            btn.type = "button";
            btn.textContent = "Shop";
            btn.disabled = !offer.clickUrl;
            btn.onclick = function () { sdk.openOffer(offer); };

            li.append(name, rate, btn);
            list.append(li);
          });
        } catch (err) {
          if (id !== requestId) return;
          list.textContent = err.status === 403
            ? "This site isn't authorised for Cashback Hive offers."
            : "Couldn't load offers.";
        }
      }

      var timer;
      search.addEventListener("input", function () {
        clearTimeout(timer);
        timer = setTimeout(function () { render(search.value); }, 250);
      });

      render("");
    }

    var sdk = window.CashbackHive;
    if (sdk && sdk.initialized) mount(sdk);
    else window.addEventListener("cbh:ready", function (e) { mount(e.detail.sdk); });
  })();
</script>
```

## React / Next.js

Requires the [`useCashbackHive()`](https://developer.cashbackhive.com/installation/nextjs) hook and the [type declarations](https://developer.cashbackhive.com/reference/types.md#typescript-declarations).

```tsx components/offers-widget.tsx theme={null}
"use client";

import { useEffect, useState } from "react";
import { useCashbackHive } from "@/hooks/use-cashback-hive";
import type { CashbackHiveSdk, CbhOffer } from "@/types/cashback-hive";

function formatCashback(offer: CbhOffer): string {
  if (!offer.cashback) return "No cashback";
  if (offer.cashback.type === "percentage") return `${offer.cashback.amount}% back`;
  return (
    new Intl.NumberFormat(undefined, {
      style: "currency",
      currency: offer.cashback.currency ?? "USD",
    }).format(offer.cashback.amount) + " back"
  );
}

function OffersList({ sdk, query }: { sdk: CashbackHiveSdk; query: string }) {
  const [offers, setOffers] = useState<CbhOffer[] | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;
    setOffers(null);
    setError(null);

    sdk.offers
      .list({ search: query || undefined, limit: 20 })
      .then((res) => {
        if (!cancelled) setOffers(res.offers);
      })
      .catch((err: unknown) => {
        if (cancelled) return;
        const status = (err as { status?: number }).status;
        setError(
          status === 403
            ? "This site isn't authorised for Cashback Hive offers."
            : "Couldn't load offers.",
        );
      });

    return () => {
      cancelled = true;
    };
  }, [sdk, query]);

  if (error) return <p role="alert">{error}</p>;
  if (offers === null) return <p>Loading…</p>;
  if (offers.length === 0) return <p>No offers found.</p>;

  return (
    <ul>
      {offers.map((offer) => (
        <li key={offer.slug}>
          {offer.logoUrl && <img src={offer.logoUrl} alt="" width={40} />}
          <strong>{offer.name}</strong> <span>{formatCashback(offer)}</span>
          <button
            type="button"
            disabled={!offer.clickUrl}
            onClick={() => sdk.openOffer(offer)}
          >
            Shop
          </button>
        </li>
      ))}
    </ul>
  );
}

export default function OffersWidget() {
  const sdk = useCashbackHive();
  const [query, setQuery] = useState("");

  if (!sdk) return <p>Loading…</p>;

  return (
    <section aria-label="Cashback offers">
      <input
        type="search"
        placeholder="Search brands"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <OffersList sdk={sdk} query={query} />
    </section>
  );
}
```

> [!TIP]
>
> In production you'll want to debounce `query` so you don't call the API on every keystroke. The vanilla example above shows a 250 ms debounce.

## Design notes

**Pinned offers come first.** `offers.list()` returns pinned offers (ones you've featured in the publisher portal) before the rest. You can render them differently by checking `offer.pinned`.

**Not every offer is clickable.** `clickUrl` can be `null` for offers that don't support click tracking. Disable the call to action for those; `openOffer()` throws if you call it on one.

**Pass a sub-ID for your own reporting.** If you want to attribute clicks to a placement, campaign or user on your side, pass `subId`:

```js theme={null}
sdk.openOffer(offer, { subId: "homepage-carousel" });
```

See [Click tracking](https://developer.cashbackhive.com/guides/click-tracking.md) for `clickId`, `subId` and `deeplink` options.

**Pagination.** Use `limit` and `offset`, and the `total` field in the response, to page through large catalogues:

```js theme={null}
const page = await sdk.offers.list({ limit: 20, offset: 40 });
console.log(page.offers.length, "of", page.total);
```

**Categories.** Filter with `category`, matching the `category` string on offers exactly:

```js theme={null}
await sdk.offers.list({ category: "Fashion and Apparel" });
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.