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

# Upsells & Offers

> Post-purchase offers, order bumps, and one-click upsells

# Upsells & Offers

Three different APIs. Use the one that matches the moment in the journey:

| When                                       | API                                         | Creates a child order?                            |
| ------------------------------------------ | ------------------------------------------- | ------------------------------------------------- |
| After checkout, one-click on the same card | `previewOffer` + **`processOfferPayment`**  | Yes — `metadata.mainOrderId` = the checkout order |
| On the checkout page, before pay           | `toggleOrderBump`                           | No — adds a line to the cart                      |
| Email / account “offers for this order”    | `listPostPurchaseOffers` + accept / decline | Different surface — **not** the OTO page          |

For the [basic post-purchase funnel](/developer-tools/funnel-demos/basic-post-purchase), use the first row only. The finished app is [`basic-post-purchase`](https://github.com/TagadaPay/examples/tree/main/basic-post-purchase).

<Warning>
  `mainOrderId` is the **checkout order id** (`result.order.id` from `processPayment`), never a payment id.
</Warning>

***

## Get an Offer

Pin IDs from the dashboard or `checkoutOffers.create` (Node SDK). Do not pick “the first upsell” from `listOffers` — a store can have many.

```typescript theme={null}
const offer = await tagada.offers.getOffer('offer_xxx');

// offer.id          -- offer ID
// offer.title       -- "Essential Cap — $19.99"
// offer.type        -- 'upsell' | 'orderbump' | 'downsell'
// offer.lineItems   -- products in the offer
```

```typescript theme={null}
const upsells = await tagada.offers.listOffers({ type: 'upsell' });
const orderBumps = await tagada.offers.listOffers({ type: 'orderbump' });
```

<Note>
  `listOffers({ type })` only filters `upsell` and `orderbump`. A downsell created with `type: 'downsell'` will not appear. Load it with `getOffer(id)`.
</Note>

***

## One-click upsell (this is the OTO)

Preview the price, then charge the instrument stored on the **main order**. Prefer `processOfferPayment` — it runs 3DS / redirect / polling the same way `payment.processPayment` does.

```typescript theme={null}
// orderId = processPayment() → result.order.id  (the tee order)

const preview = await tagada.offers.previewOffer({ offerId: 'offer_cap' });
// preview.items, preview.totalAmount, preview.currency

const result = await tagada.offers.processOfferPayment({
  offerId: 'offer_cap',
  mainOrderId: orderId,
  returnUrl: window.location.href,
});

switch (result.status) {
  case 'succeeded':
    // Child order exists. Next OTO or thank you — your router.
    window.location.href = `/offer-tote?orderId=${orderId}`;
    break;
  case 'requires_redirect':
    window.location.href = result.redirectUrl;
    break;
  case 'failed':
    console.error(result.error);
    break;
}
```

Decline is not an API call on this path. Send them to the next URL yourself (`/downsell?orderId=` or `/thank-you?orderId=`).

### Low-level: `payPreviewedOffer`

Returns the raw `{ payment, order, preview, checkout }` and does **not** finish 3DS. Only use it if you handle `payment.requireAction` yourself. For a storefront, use `processOfferPayment`.

***

## Thank you: related orders

The accept does **not** add a line to the tee order. It creates a **child order** — and it lands asynchronously, a couple of seconds after the redirect. The SDK handles the wait for you:

```typescript theme={null}
// Vanilla — polls until the child orders exist (or 8s, whichever first)
const order = await tagada.customer.getOrder(orderId, { waitForRelatedOrders: true });
// order.items            → tee
// order.relatedOrders    → cap, tote, … one per accepted offer
```

```tsx theme={null}
// React — renders the tee immediately, upsells pop in when they land
const { order, relatedOrders } = useOrder(orderId);
```

See [`useOrder`](/developer-tools/headless-sdk/customer#useorder--thank-you-pages) for the full thank-you page. Full graph: [Basic post-purchase](/developer-tools/funnel-demos/basic-post-purchase).

***

## Order Bumps

Shown **on** checkout, before payment:

```typescript theme={null}
await tagada.offers.toggleOrderBump({
  checkoutSessionId: session.id,
  orderBumpOfferId: bumps[0].id,
  selected: true,
});
```

The parameter is `orderBumpOfferId` (not `offerId`).

***

## Post-purchase offer list (not the OTO page)

`listPostPurchaseOffers` / `acceptPostPurchaseOffer` / `declinePostPurchaseOffer` are for “offers attached to this order” (account, email). They are **not** what the cap/tote pages in the basic funnel call.

***

## React Hook

```tsx theme={null}
import { useOffers } from '@tagadapay/headless-sdk/react';

function UpsellPage({ offerId, orderId }: { offerId: string; orderId: string }) {
  const { offer, getOffer, previewOffer, processOfferPayment, isLoading } = useOffers();

  useEffect(() => { getOffer(offerId); }, [offerId]);

  const accept = async () => {
    const result = await processOfferPayment({
      offerId,
      mainOrderId: orderId,
      returnUrl: window.location.href,
    });
    if (result.status === 'succeeded') {
      window.location.href = `/offer-tote?orderId=${orderId}`;
    }
  };

  if (isLoading) return <p>Loading...</p>;

  return (
    <div>
      <h2>{offer?.title}</h2>
      <button onClick={accept}>Yes, add it</button>
      <button onClick={() => { window.location.href = `/downsell?orderId=${orderId}`; }}>
        No thanks
      </button>
    </div>
  );
}
```

### Full Hook API

| Method                                                               | Description                                         |
| -------------------------------------------------------------------- | --------------------------------------------------- |
| `getOffer(offerId)`                                                  | Load a single offer (sets `offer` state)            |
| `listOffers({ type? })`                                              | List offers filtered by `'upsell'` or `'orderbump'` |
| `previewOffer({ offerId })`                                          | Preview pricing (sets `preview` state)              |
| `processOfferPayment({ offerId, mainOrderId, returnUrl? })`          | One-click charge + 3DS / polling                    |
| `payPreviewedOffer({ offerId, mainOrderId })`                        | Raw charge only — no 3DS handling                   |
| `toggleOrderBump({ checkoutSessionId, orderBumpOfferId, selected })` | Toggle a bump on/off                                |
| `listPostPurchaseOffers(orderId)`                                    | List account-level post-purchase offers             |
| `acceptPostPurchaseOffer({ orderId, offerId })`                      | Accept one of those (not the OTO page)              |
| `declinePostPurchaseOffer({ orderId, offerId })`                     | Decline one of those                                |
