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

# Display offer details

> Load full offer details with the React SDK or the API.

Fetch full offer details — pricing options, legal terms, milestones, and more — for a single offer the user can access.

## Prerequisites

* Completed [OAuth](/sdk/oauth-authentication) and a working `CoinListProvider`
* An **offer id** from [Fetch offers](/sdk/fetch-offers) (for example `offer.id` from `OffersGrid` `onOfferClick`)

## Recommended: useOfferDetails

The hook mirrors the SDK client method `fetchOfferDetails`: it waits until the provider is ready, then loads details and exposes `offerDetailsState` (`LOADING`, `ERROR`, or `CONTENT`).

When the user picks an offer from the grid, keep that `offer` in React state and pass `offer.id` - TypeScript then matches the branded id type the hook expects.

```tsx theme={null}
"use client";

import type { ComponentProps } from "react";
import { useState } from "react";
import { OffersGrid, useOfferDetails } from "@coinlist-co/react";

type OfferFromGrid = Parameters<
  NonNullable<ComponentProps<typeof OffersGrid>["onOfferClick"]>
>[0];

function OfferDetailPanel({ offer }: { offer: OfferFromGrid }) {
  const { offerDetailsState } = useOfferDetails(offer.id);

  if (offerDetailsState.type === "LOADING") return <p>Loading details…</p>;
  if (offerDetailsState.type === "ERROR") return <p>Unable to load this offer.</p>;

  const detail = offerDetailsState.offerDetail;
  return (
    <section>
      <h1>{detail.name}</h1>
      {/* Render options, terms, milestones from detail — see API reference for fields */}
    </section>
  );
}

export function OfferBrowseAndDetail() {
  const [selected, setSelected] = useState<OfferFromGrid | null>(null);

  return (
    <>
      <OffersGrid onOfferClick={(offer) => setSelected(offer)} />
      {selected ? <OfferDetailPanel offer={selected} /> : null}
    </>
  );
}
```

## REST: GET /v1/offers/{id}

Use the API directly when you are not in React or you need a one-off server fetch with a token you already hold.

<AccordionGroup>
  <Accordion title="Example: fetch with Bearer token">
    ```ts theme={null}
    // lib/coinlist-api.ts
    export async function getOfferDetails(offerId: string, accessToken: string) {
      const res = await fetch(
        `https://api.coinlist.co/v1/offers/${encodeURIComponent(offerId)}`,
        {
          headers: { Authorization: `Bearer ${accessToken}` },
        },
      );

      if (!res.ok) {
        const error = await res.json();
        throw new Error(error.message ?? "Failed to fetch offer");
      }

      return res.json();
    }
    ```

    See the [API reference](/api-reference) for the exact response schema.
  </Accordion>
</AccordionGroup>

Example response shape (values are illustrative - always rely on the live API and legal URLs from Passage):

```json theme={null}
{
  "object": "offer_details",
  "id": "{offer_id}",
  "slug": "example-offer",
  "name": "Example Token Sale",
  "starts_at": "2026-01-01T00:00:00.000000Z",
  "ends_at": "2026-01-31T00:00:00.000000Z",
  "options": [
    {
      "id": "{option_id}",
      "slug": "standard",
      "price_usd": 0.1,
      "floor_price_usd": 0.08,
      "minimum_purchase_usd": 100,
      "total_token_supply": 1000000,
      "sale_agreement_url": "https://coinlist.co/agreements/example"
    }
  ],
  "terms": [
    { "key": "Vesting", "value": "12 months linear" },
    { "key": "Lockup", "value": "6 months cliff" }
  ],
  "milestones": [
    { "name": "Registration", "schedule": "Jan 1", "status": "completed" },
    { "name": "Sale Open", "schedule": "Jan 15", "status": "active" },
    { "name": "Distribution", "schedule": "Feb 1", "status": "upcoming" }
  ],
  "faqs": [],
  "links": []
}
```

<Note>
  Display the full terms and link to the sale agreement URL as provided — do not summarize or paraphrase legal terms.
</Note>

## Lower-level client

<AccordionGroup>
  <Accordion title="coinlist.fetchOfferDetails in React">
    Equivalent to the hook, without the built-in state machine. Pass the same `offer.id` you get from `OffersGrid`:

    ```tsx theme={null}
    "use client";

    import type { ComponentProps } from "react";
    import { OffersGrid, useCoinList } from "@coinlist-co/react";
    import { useEffect, useState } from "react";

    type OfferFromGrid = Parameters<
      NonNullable<ComponentProps<typeof OffersGrid>["onOfferClick"]>
    >[0];

    export function OfferDetailManual({ offer }: { offer: OfferFromGrid }) {
      const { coinlist, isReady } = useCoinList();
      const [detail, setDetail] = useState<unknown>(null);

      useEffect(() => {
        if (!isReady) return;
        void coinlist
          .fetchOfferDetails(offer.id)
          .then(setDetail)
          .catch(() => setDetail(null));
      }, [coinlist, isReady, offer.id]);

      if (!isReady) return <p>Loading…</p>;
      return <pre>{JSON.stringify(detail, null, 2)}</pre>;
    }
    ```

    Prefer `useOfferDetails` unless you need custom control flow.
  </Accordion>
</AccordionGroup>

## Next step

<Card title="Create and track participations" icon="wallet" href="/sdk/participations" horizontal>
  Create a participation for an offer option, then monitor participation status.
</Card>
