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

# Basic post-purchase

> The simplest funnel type: landing, checkout, one-click upsell, downsell, a second offer, then thank you with related orders

# Basic post-purchase

**Time**: \~15 minutes | **Difficulty**: Beginner | **Type**: first demo in [Funnel demos](/developer-tools/funnel-demos/introduction)

<Info>
  **Finished project:** [`basic-post-purchase`](https://github.com/TagadaPay/examples/tree/main/basic-post-purchase). Clone it if you want the working app first — or keep reading and rebuild the same graph.
</Info>

Landing → pay → one-click add-on → cheaper fallback if they decline → one more add-on → thank you. This is the default DTC pattern. Everything else in this category is a variation on this graph.

<img src="https://mintcdn.com/tagadapay/kGWh0luIQjcrt2Rd/assets/images/funnel-demos/basic-post-purchase-graph.png?fit=max&auto=format&n=kGWh0luIQjcrt2Rd&q=85&s=ca8b7f3094c09e2bfc8053275990cefc" alt="Basic post-purchase funnel: landing, checkout, cap OTO, cap downsell, tote OTO, thank you" width="3168" height="404" data-path="assets/images/funnel-demos/basic-post-purchase-graph.png" />

<Info>
  The live Tagada Demo store ships this type as **Studio Showcase** (Essential Tee → cap → tote). Reproduce it on any store — swap the products, keep the edges.
</Info>

***

## What you need

| Resource          | Role                                          | Example                                                    |
| ----------------- | --------------------------------------------- | ---------------------------------------------------------- |
| Main product      | Cart on landing / checkout                    | Essential Tee · \$29.99                                    |
| Upsell product    | First one-click offer                         | Essential Cap · \$19.99                                    |
| Downsell product  | Same item, lower price                        | Essential Cap · \$14.99                                    |
| Second upsell     | Shown after accept *or* after downsell accept | Essential Tote · \$24                                      |
| 3 checkout offers | Bound on the three `offer` steps              | `type: 'upsell'` (downsell can also be `type: 'downsell'`) |

Prices are **integer cents** (`2999` = \$29.99).

***

## The graph

Six steps. Conditions only leave the offer nodes.

| Step         | `type`     | Path                   | Bound offer |
| ------------ | ---------- | ---------------------- | ----------- |
| Landing      | `landing`  | `/`                    | —           |
| Checkout     | `checkout` | `/checkout`            | —           |
| Cap OTO      | `offer`    | `/offer/:orderId`      | Cap \$19.99 |
| Cap downsell | `offer`    | `/downsell/:orderId`   | Cap \$14.99 |
| Tote OTO     | `offer`    | `/offer-tote/:orderId` | Tote \$24   |
| Thank you    | `thankyou` | `/thankyou/:orderId`   | —           |

| Edge                     | `when`                     |
| ------------------------ | -------------------------- |
| Landing → Checkout       | (none)                     |
| Checkout → Cap OTO       | `payment.success`          |
| Cap OTO → Tote OTO       | `offer.accepted`           |
| Cap OTO → Cap downsell   | `offer.declined`           |
| Cap downsell → Tote OTO  | `offer.accepted`           |
| Cap downsell → Thank you | `offer.declined`           |
| Tote OTO → Thank you     | (none — accept or decline) |

`/:orderId` on offer and thank-you paths is what lets the next page load the main order (and then the related ones).

***

## Thank you: related orders

Post-purchase accepts do **not** mutate the main order. Each accept creates a **child order** whose `metadata.mainOrderId` is the checkout order.

`GET` the main order (Plugin SDK `getOrder`, Headless `tagada.customer.getOrder`, dashboard order API). The payload includes `relatedOrders[]` — one entry per accepted offer.

```ts theme={null}
const order = await tagada.customer.getOrder(orderId, { waitForRelatedOrders: true });
// order.items              → tee
// order.relatedOrders[0]   → cap (if they accepted the OTO)
// order.relatedOrders[1]   → tote (if they accepted the second offer)
```

<Warning>
  Accept navigates immediately, but the child order lands **asynchronously** — the first response is often missing it. `waitForRelatedOrders: true` makes the SDK poll for up to 8s and return the latest snapshot. In React, [`useOrder`](/developer-tools/headless-sdk/customer#useorder--thank-you-pages) does the same in the background so the main order paints immediately.
</Warning>

Studio’s `OrderItemsList` already renders each related order as its own block and a combined total. If you build the thank-you page yourself, do the same: one block per `order` + each `relatedOrders[]` item.

That is the system. Do not invent a second cart or append line items onto the original order.

***

## Build it in Studio (matches the demo)

<Steps>
  <Step title="Catalog">
    Create the four products (or three if downsell reuses the cap at a second price). Each offer needs its own `priceId`.
  </Step>

  <Step title="Checkout offers">
    Dashboard → Offers: one upsell (cap), one downsell (cap cheaper), one upsell (tote). Each offer is a single line item.
  </Step>

  <Step title="Funnel">
    New funnel. Add the six steps above. Draw the seven edges. Bind each offer on its step (`stepConfig.resources.offer`).
  </Step>

  <Step title="Pages">
    Assign a Studio page per step — or leave checkout / offer / thank you empty to get **native** pages. Landing needs a Studio (or Plugin) page: native checkout does not cover `landing`.
  </Step>

  <Step title="Landing CTA">
    The buy button must open checkout with the tee `lineItems` (`variantId` + quantity). A header “Checkout” with an empty cart shows placeholder totals.
  </Step>

  <Step title="Save">
    Saving mounts routes. Preview from the landing step with a session that already has the tee in the cart.
  </Step>
</Steps>

Offer pages that share the Plugin/Studio internal path `/offer` need a **path remap** to `/downsell` and `/offer-tote` so decline/accept do not land on the same URL. See [Path remapping](/developer-tools/sdk/path-remapping).

***

## Build it with the Node SDK

Same graph as Studio. Native pages are injected when a step has no `pluginId`.

```ts theme={null}
import Tagada from '@tagadapay/node-sdk';

const tagada = new Tagada('your-api-key');
const STORE_ID = 'store_...';

// 1. Products — tee, cap, cap-sale, tote (see Node SDK upsell tutorial for the full create shape)
const tee = await tagada.products.create({ /* $29.99 */ });
const cap = await tagada.products.create({ /* $19.99 */ });
const capSale = await tagada.products.create({ /* $14.99 */ });
const tote = await tagada.products.create({ /* $24.00 */ });

const price = (product: { variants: { prices: { id: string }[] }[] }) =>
  product.variants[0].prices[0].id;

// 2. Offers
const capOffer = await tagada.checkoutOffers.create({
  storeId: STORE_ID,
  type: 'upsell',
  titleTrans: { en: 'Essential Cap — $19.99' },
  lineItems: [{ priceId: price(cap), quantity: 1 }],
});
const capDownsell = await tagada.checkoutOffers.create({
  storeId: STORE_ID,
  type: 'upsell',
  titleTrans: { en: 'Essential Cap — $14.99' },
  lineItems: [{ priceId: price(capSale), quantity: 1 }],
});
const toteOffer = await tagada.checkoutOffers.create({
  storeId: STORE_ID,
  type: 'upsell',
  titleTrans: { en: 'Essential Tote — $24' },
  lineItems: [{ priceId: price(tote), quantity: 1 }],
});

// 3. Funnel
const funnel = await tagada.funnels.create({
  storeId: STORE_ID,
  config: {
    name: 'Basic post-purchase',
    version: '1.0.0',
    nodes: [
      {
        id: 'step_landing',
        name: 'Landing',
        kind: 'step',
        type: 'landing',
        isEntry: true,
        position: { x: 0, y: 0 },
        config: { path: '/' },
      },
      {
        id: 'step_checkout',
        name: 'Checkout',
        kind: 'step',
        type: 'checkout',
        position: { x: 400, y: 0 },
        config: { path: '/checkout' },
      },
      {
        id: 'step_cap',
        name: 'Cap OTO',
        kind: 'step',
        type: 'offer',
        position: { x: 800, y: 0 },
        config: {
          path: '/offer/:orderId',
          stepConfig: { resources: { offer: capOffer.id } },
        },
      },
      {
        id: 'step_downsell',
        name: 'Cap downsell',
        kind: 'step',
        type: 'offer',
        position: { x: 800, y: 180 },
        config: {
          path: '/downsell/:orderId',
          stepConfig: { resources: { offer: capDownsell.id } },
        },
      },
      {
        id: 'step_tote',
        name: 'Tote OTO',
        kind: 'step',
        type: 'offer',
        position: { x: 1200, y: 0 },
        config: {
          path: '/offer-tote/:orderId',
          stepConfig: { resources: { offer: toteOffer.id } },
        },
      },
      {
        id: 'step_thankyou',
        name: 'Thank You',
        kind: 'step',
        type: 'thankyou',
        position: { x: 1600, y: 0 },
        config: { path: '/thankyou/:orderId' },
      },
    ],
    edges: [
      { id: 'e_land', source: 'step_landing', target: 'step_checkout' },
      {
        id: 'e_pay',
        source: 'step_checkout',
        target: 'step_cap',
        conditions: { when: 'payment.success' },
      },
      {
        id: 'e_cap_yes',
        source: 'step_cap',
        target: 'step_tote',
        conditions: { when: 'offer.accepted' },
      },
      {
        id: 'e_cap_no',
        source: 'step_cap',
        target: 'step_downsell',
        conditions: { when: 'offer.declined' },
      },
      {
        id: 'e_ds_yes',
        source: 'step_downsell',
        target: 'step_tote',
        conditions: { when: 'offer.accepted' },
      },
      {
        id: 'e_ds_no',
        source: 'step_downsell',
        target: 'step_thankyou',
        conditions: { when: 'offer.declined' },
      },
      { id: 'e_tote', source: 'step_tote', target: 'step_thankyou' },
    ],
  },
});

await tagada.funnels.update(funnel.id, { storeId: STORE_ID, config: funnel.config });
```

<Note>
  Native pages cover `checkout`, `offer`, and `thankyou`. The landing step still needs a Studio or Plugin page (or skip landing and make checkout `isEntry: true` — then this type collapses to the [4-step Node tutorial](/developer-tools/node-sdk/upsell-downsell-funnel)).
</Note>

Product `create` payload: [Upsell & Downsell Funnel → Step 2](/developer-tools/node-sdk/upsell-downsell-funnel#step-2-create-products).

Share a cart link:

```ts theme={null}
const session = await tagada.checkout.createSession({
  storeId: STORE_ID,
  items: [{ variantId: tee.variants[0].id, quantity: 1 }],
  currency: 'USD',
  checkoutUrl, // from the mounted checkout node
});
```

***

## Same type on the Headless SDK

You host every URL. Tagada does not move the shopper. Copy this sequence — it is the whole type.

```ts theme={null}
import { createHeadlessClient } from '@tagadapay/headless-sdk';

const tagada = createHeadlessClient({
  storeId: 'store_xxx',
  environment: 'production',
});

// 1. Landing CTA
const { url } = await tagada.checkout.createSessionUrl({
  items: [{ variantId: 'variant_tee', quantity: 1 }],
  currency: 'USD',
  checkoutPath: '/checkout',
});
window.location.href = url;

// 2. /checkout — tokens from the query string
const tokens = tagada.checkout.parseTokensFromUrl();
if (!tokens) throw new Error('no checkoutToken in URL — send the shopper back to the landing');
const session = await tagada.checkout.loadSession(tokens.checkoutToken, tokens.sessionToken);
await tagada.checkout.updateCustomer(session.id, { email, firstName, lastName });
await tagada.checkout.updateAddress(session.id, { shippingAddress });
const { tagadaToken } = await tagada.payment.tokenizeCard({ cardNumber, expiryDate, cvc });
const paid = await tagada.payment.processPayment({
  checkoutSessionId: session.id,
  tagadaToken,
});
if (paid.status !== 'succeeded' || !paid.order?.id) throw new Error('pay failed');
const orderId = paid.order.id;
window.location.href = `/offer?orderId=${orderId}`;

// 3. /offer — pin the cap offer id
const oto = await tagada.offers.processOfferPayment({
  offerId: 'offer_cap',
  mainOrderId: orderId,
  returnUrl: window.location.href,
});
window.location.href = oto.status === 'succeeded'
  ? `/offer-tote?orderId=${orderId}`
  : `/downsell?orderId=${orderId}`; // or just go here on "No thanks"

// 4. /thank-you — waits for the upsell child orders (max 8s), never throws
const order = await tagada.customer.getOrder(orderId, { waitForRelatedOrders: true });
// order.items + order.relatedOrders — one child order per accepted offer
```

In React, the thank-you page is one hook:

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

const { order, relatedOrders } = useOrder(orderId);
// Main order paints immediately; upsells pop in when their child orders land.
```

Offers are created with the Node SDK (`checkoutOffers.create`) or the dashboard. The Headless SDK only **charges** them.

Full method docs: [Checkout Flow](/developer-tools/headless-sdk/checkout-flow), [Upsells & Offers](/developer-tools/headless-sdk/offers).

To let Tagada own the step URLs instead, create the graph above and send the shopper in with [`funnel.navigate()`](/developer-tools/headless-sdk/funnel-navigation). That is optional — a self-hosted store does not need a funnel record.

***

## Paths the customer can take

| Path                                               | What they paid          |
| -------------------------------------------------- | ----------------------- |
| Pay → accept cap → accept tote                     | Tee + cap + tote        |
| Pay → accept cap → decline tote                    | Tee + cap               |
| Pay → decline cap → accept downsell → accept tote  | Tee + cap (sale) + tote |
| Pay → decline cap → accept downsell → decline tote | Tee + cap (sale)        |
| Pay → decline cap → decline downsell               | Tee only                |

Every accepted offer appears on thank you as a **related order**, not as an extra line on the tee order.

***

## Next

<CardGroup cols={2}>
  <Card title="Finished project on GitHub" icon="github" href="https://github.com/TagadaPay/examples/tree/main/basic-post-purchase">
    Clone `basic-post-purchase` and run it in a minute
  </Card>

  <Card title="Funnel demos" icon="layer-group" href="/developer-tools/funnel-demos/introduction">
    Catalog of types — this is the basic one
  </Card>

  <Card title="Funnel Orchestrator" icon="diagram-project" href="/developer-tools/funnels/funnel-orchestrator">
    Nodes, edges, analytics, A/B
  </Card>

  <Card title="Node SDK 4-step tutorial" icon="arrow-up-right-dots" href="/developer-tools/node-sdk/upsell-downsell-funnel">
    Same idea without landing or the second OTO
  </Card>

  <Card title="Headless offers" icon="plug" href="/developer-tools/headless-sdk/offers">
    One-click pay with `mainOrderId`
  </Card>
</CardGroup>
