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

# Upsell & Downsell Funnel

> Build a checkout flow with conditional offer routing — upsell on accept, downsell on decline

# Upsell & Downsell Funnel

**Time**: \~10 minutes | **Difficulty**: Intermediate

Build a complete post-purchase offer flow using the Node SDK. Customers check out, see an upsell offer, and either accept it (→ thank you) or decline it (→ cheaper downsell offer → thank you).

```
Checkout → Upsell $99 ──(accepted)──→ Thank You
                        ──(declined)──→ Downsell $19 → Thank You
```

<Info>
  This tutorial uses **native checkout pages** — no custom HTML or Plugin SDK deployment needed. TagadaPay auto-injects checkout, offer, and thank you pages when no plugin is assigned to a step.
</Info>

***

## Prerequisites

| Requirement                                 | How to get it                                                         |
| ------------------------------------------- | --------------------------------------------------------------------- |
| Node.js 18+                                 | [nodejs.org](https://nodejs.org)                                      |
| `@tagadapay/node-sdk`                       | `npm install @tagadapay/node-sdk`                                     |
| API key                                     | Dashboard → Settings → API Keys                                       |
| A store with at least one payment processor | [Merchant Quick Start](/developer-tools/node-sdk/merchant-quickstart) |

***

## What You'll Build

A 4-step funnel with **conditional routing** based on offer acceptance:

| Step      | Type       | Purpose                                                         |
| --------- | ---------- | --------------------------------------------------------------- |
| Checkout  | `checkout` | Payment page — collects card info, processes the initial order  |
| Upsell    | `offer`    | Post-purchase offer — VIP Coaching Pack (\$99)                  |
| Downsell  | `offer`    | Fallback offer if upsell is declined — Quick-Start Guide (\$19) |
| Thank You | `thankyou` | Order confirmation — fires conversion events                    |

The edges between steps use **conditions** to route the customer:

* `{ when: 'offer.accepted' }` — customer accepted the offer
* `{ when: 'offer.declined' }` — customer declined the offer

***

## Step 1: Initialize the SDK

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

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

***

## Step 2: Create products

You need three products — one for the main checkout, one for the upsell, one for the downsell.

```ts theme={null}
const mainProduct = await tagada.products.create({
  storeId: STORE_ID,
  name: 'Premium Course',
  description: 'The main product customers purchase at checkout.',
  active: true,
  isShippable: false,
  isTaxable: false,
  variants: [{
    name: 'Default',
    sku: 'COURSE-001',
    grams: null,
    active: true,
    default: true,
    price: null,
    compareAtPrice: null,
    prices: [{
      currencyOptions: { USD: { amount: 4900 } },
      recurring: false,
      billingTiming: 'usage',
      interval: null,
      intervalCount: 1,
      default: true,
    }],
  }],
});

const upsellProduct = await tagada.products.create({
  storeId: STORE_ID,
  name: 'VIP Coaching Pack',
  active: true,
  isShippable: false,
  isTaxable: false,
  variants: [{
    name: 'Default',
    sku: 'VIP-001',
    grams: null,
    active: true,
    default: true,
    price: null,
    compareAtPrice: null,
    prices: [{
      currencyOptions: { USD: { amount: 9900 } },
      recurring: false,
      billingTiming: 'usage',
      interval: null,
      intervalCount: 1,
      default: true,
    }],
  }],
});

const downsellProduct = await tagada.products.create({
  storeId: STORE_ID,
  name: 'Quick-Start Guide',
  active: true,
  isShippable: false,
  isTaxable: false,
  variants: [{
    name: 'Default',
    sku: 'GUIDE-001',
    grams: null,
    active: true,
    default: true,
    price: null,
    compareAtPrice: null,
    prices: [{
      currencyOptions: { USD: { amount: 1900 } },
      recurring: false,
      billingTiming: 'usage',
      interval: null,
      intervalCount: 1,
      default: true,
    }],
  }],
});
```

<Tip>
  Prices are in **cents** — `4900` = \$49.00. The `currencyOptions` object lets you define amounts per currency.
</Tip>

***

## Step 3: Create checkout offers

Checkout offers are standalone resources that you bind to funnel steps. Each offer references product line items via their `priceId`.

Use `tagada.checkoutOffers.create()` — this creates offers in the checkout offers table, which the native offer page can render.

```ts theme={null}
const upsellOffer = await tagada.checkoutOffers.create({
  storeId: STORE_ID,
  type: 'upsell',
  titleTrans: { en: 'Upgrade to VIP Coaching Pack' },
  lineItems: [{
    priceId: upsellProduct.variants[0].prices[0].id,
    quantity: 1,
  }],
});

const downsellOffer = await tagada.checkoutOffers.create({
  storeId: STORE_ID,
  type: 'upsell',
  titleTrans: { en: 'Get the Quick-Start Guide instead' },
  lineItems: [{
    priceId: downsellProduct.variants[0].prices[0].id,
    quantity: 1,
  }],
});
```

### Verify and list offers

```ts theme={null}
// Retrieve a single offer
const offer = await tagada.checkoutOffers.retrieve(upsellOffer.id);
console.log('Offer title:', offer.offer.titleTrans.en);

// List all offers for the store
const allOffers = await tagada.checkoutOffers.list({ storeId: STORE_ID });
console.log('Total offers:', allOffers.offers.length);
```

<Note>
  Both upsell and downsell use `type: 'upsell'` — they are post-purchase offers. For order bumps shown **on** the checkout page (before payment), use `tagada.offers.create()` with `type: 'orderbump'`.
</Note>

***

## Step 4: Create the funnel with conditional edges

This is where the magic happens. Define four nodes and five edges with `conditions.when` to control the flow.

```ts theme={null}
const funnel = await tagada.funnels.create({
  storeId: STORE_ID,
  config: {
    name: 'Upsell/Downsell Flow',
    version: '1.0.0',
    nodes: [
      {
        id: 'step_checkout',
        name: 'Checkout',
        kind: 'step',
        type: 'checkout',
        isEntry: true,
        position: { x: 0, y: 150 },
        index: 0,
        config: {
          pagePath: '/checkout',
          metadata: { internalPath: '/checkout' },
          stepConfig: {
            orderBumps: { mode: 'inherit' },
          },
        },
      },
      {
        id: 'step_upsell',
        name: 'Upsell — VIP Coaching',
        kind: 'step',
        type: 'offer',
        position: { x: 300, y: 150 },
        index: 1,
        config: {
          pagePath: '/upsell',
          metadata: { internalPath: '/offer' },
          stepConfig: {
            resources: { offer: upsellOffer.id },
          },
        },
      },
      {
        id: 'step_downsell',
        name: 'Downsell — Quick-Start Guide',
        kind: 'step',
        type: 'offer',
        position: { x: 600, y: 300 },
        index: 2,
        config: {
          pagePath: '/downsell',
          metadata: { internalPath: '/offer' },
          stepConfig: {
            resources: { offer: downsellOffer.id },
          },
        },
      },
      {
        id: 'step_thankyou',
        name: 'Thank You',
        kind: 'step',
        type: 'thankyou',
        position: { x: 900, y: 150 },
        index: 3,
        config: {
          pagePath: '/thankyou/:orderId',
          metadata: { internalPath: '/thankyou/:orderId' },
        },
      },
    ],
    edges: [
      // Checkout → Upsell (always, after payment)
      {
        id: 'e_checkout_to_upsell',
        source: 'step_checkout',
        target: 'step_upsell',
        label: 'After payment',
      },
      // Upsell accepted → Thank You
      {
        id: 'e_upsell_accepted',
        source: 'step_upsell',
        target: 'step_thankyou',
        label: 'Offer accepted',
        conditions: { when: 'offer.accepted' },
      },
      // Upsell declined → Downsell
      {
        id: 'e_upsell_declined',
        source: 'step_upsell',
        target: 'step_downsell',
        label: 'Offer declined',
        conditions: { when: 'offer.declined' },
      },
      // Downsell accepted → Thank You
      {
        id: 'e_downsell_accepted',
        source: 'step_downsell',
        target: 'step_thankyou',
        label: 'Downsell accepted',
        conditions: { when: 'offer.accepted' },
      },
      // Downsell declined → Thank You
      {
        id: 'e_downsell_declined',
        source: 'step_downsell',
        target: 'step_thankyou',
        label: 'Downsell declined',
        conditions: { when: 'offer.declined' },
      },
    ],
  },
  isDefault: false,
});
```

### How conditional edges work

Each offer step has two possible outcomes: **accepted** or **declined**. The orchestrator checks the edge conditions to decide where to send the customer:

```
step_upsell
  ├── conditions: { when: 'offer.accepted' }  → step_thankyou   (customer took the VIP pack)
  └── conditions: { when: 'offer.declined' }  → step_downsell   (show cheaper alternative)
```

If both edges point to the same target (e.g., downsell → thank you regardless of outcome), the customer always moves forward but the system still tracks what they chose.

***

## Step 5: Activate the funnel

Creating a funnel defines the structure. **Updating** it triggers the routing engine — routes get mounted to CDN, pages become reachable.

```ts theme={null}
const result = await tagada.funnels.update(funnel.id, {
  storeId: STORE_ID,
  config: funnel.config,
});

// The checkout URL is now live
const checkoutUrl = result.funnel.config.nodes
  .find(n => n.id === 'step_checkout').config.url;

console.log('Checkout live at:', checkoutUrl);
```

<Info>
  TagadaPay detects that your nodes have no `pluginId` and automatically injects the **native checkout** plugin. The offer pages show the bound offer product, and the thank you page shows the order confirmation — no setup needed.
</Info>

***

## Step 6: Verify and manage the funnel

### Retrieve a funnel

```ts theme={null}
const retrieved = await tagada.funnels.retrieve(funnel.id);
const nodes = retrieved.config.nodes;
const edges = retrieved.config.edges;

const upsellNode = nodes.find(n => n.id === 'step_upsell');
console.log('Upsell offer:', upsellNode.config.stepConfig.resources.offer);

const downsellNode = nodes.find(n => n.id === 'step_downsell');
console.log('Downsell offer:', downsellNode.config.stepConfig.resources.offer);
```

### List all funnels

```ts theme={null}
const funnels = await tagada.funnels.list(STORE_ID);
console.log(`${funnels.funnels.length} funnels in store`);
```

### Delete a funnel

```ts theme={null}
await tagada.funnels.del(funnel.id, STORE_ID);
console.log('Funnel deleted');
```

### Delete checkout offers

```ts theme={null}
await tagada.checkoutOffers.del({
  offerIds: [upsellOffer.id, downsellOffer.id],
  storeId: STORE_ID,
});
```

***

## Complete Flow Diagram

```
┌──────────────┐     ┌────────────────────┐     ┌──────────────┐
│   Checkout   │────▶│   Upsell ($99)     │────▶│  Thank You   │
│   ($49.00)   │     │   VIP Coaching     │     │              │
└──────────────┘     └─────────┬──────────┘     └──────▲───────┘
                        declined│                       │
                               ▼                       │
                     ┌────────────────────┐            │
                     │  Downsell ($19)    │────────────┘
                     │  Quick-Start Guide │
                     └────────────────────┘
```

**Customer paths:**

1. **Checkout → Accept upsell → Thank You** — Customer pays $49 + $99 = \$148
2. **Checkout → Decline upsell → Accept downsell → Thank You** — Customer pays $49 + $19 = \$68
3. **Checkout → Decline upsell → Decline downsell → Thank You** — Customer pays \$49

***

## Edge Conditions Reference

The funnel orchestrator supports a rich set of conditions you can attach to edges via `conditions.when`. Use **string** format for simple conditions, or **object** format for conditions that take parameters.

### Offer Conditions

| Condition              | Format   | Description                        |
| ---------------------- | -------- | ---------------------------------- |
| `offer.accepted`       | `string` | Customer accepted the offer        |
| `offer.declined`       | `string` | Customer declined the offer        |
| `offer.acceptedWithId` | `object` | Customer accepted a specific offer |

```ts theme={null}
// Simple: any offer accepted
conditions: { when: 'offer.accepted' }

// Parameterized: specific offer accepted
conditions: { when: { 'offer.acceptedWithId': { offerId: 'offer_xxx' } } }
```

### Payment Conditions

| Condition                   | Format   | Params           | Description                                |
| --------------------------- | -------- | ---------------- | ------------------------------------------ |
| `payment.success`           | `string` | —                | Payment succeeded                          |
| `payment.failed`            | `string` | —                | Payment failed (error, declined, rejected) |
| `payment.amountGreaterThan` | `object` | `amount` (cents) | Payment amount exceeds threshold           |

```ts theme={null}
conditions: { when: 'payment.success' }
conditions: { when: 'payment.failed' }
conditions: { when: { 'payment.amountGreaterThan': { amount: 5000 } } }  // > $50
```

### Main Order Conditions

Evaluate the **initial checkout order** (the first order in the funnel).

| Condition                    | Format   | Params                      | Description                       |
| ---------------------------- | -------- | --------------------------- | --------------------------------- |
| `mainOrder.exists`           | `string` | —                           | Main order was created            |
| `mainOrder.hasProduct`       | `object` | `productId` or `variantIds` | Order contains a specific product |
| `mainOrder.totalGreaterThan` | `object` | `amount` (cents)            | Order total exceeds threshold     |

```ts theme={null}
conditions: { when: 'mainOrder.exists' }
conditions: { when: { 'mainOrder.hasProduct': { productId: 'prod_xxx' } } }
conditions: { when: { 'mainOrder.totalGreaterThan': { amount: 10000 } } }  // > $100
```

### Last Order Conditions

Evaluate the **most recent order** (from the current or previous step — could be an upsell or downsell order).

| Condition                    | Format   | Params                      | Description                        |
| ---------------------------- | -------- | --------------------------- | ---------------------------------- |
| `lastOrder.exists`           | `string` | —                           | Last order exists                  |
| `lastOrder.hasProduct`       | `object` | `productId` or `variantIds` | Last order contains a product      |
| `lastOrder.totalGreaterThan` | `object` | `amount` (cents)            | Last order total exceeds threshold |

```ts theme={null}
conditions: { when: 'lastOrder.exists' }
conditions: { when: { 'lastOrder.hasProduct': { productId: 'prod_xxx' } } }
conditions: { when: { 'lastOrder.totalGreaterThan': { amount: 2000 } } }  // > $20
```

### Customer Tag Conditions

Route based on tags attached to the customer (set via API or automations).

| Condition               | Format   | Params                   | Description                             |
| ----------------------- | -------- | ------------------------ | --------------------------------------- |
| `customer.hasTag`       | `object` | `tag`                    | Customer has an exact tag               |
| `customer.hasTagPrefix` | `object` | `prefix`                 | Customer has a tag starting with prefix |
| `customer.hasAnyTag`    | `object` | `tags` (comma-separated) | Customer has at least one of the tags   |
| `customer.hasAllTags`   | `object` | `tags` (comma-separated) | Customer has all of the tags            |

```ts theme={null}
conditions: { when: { 'customer.hasTag': { tag: 'vip' } } }
conditions: { when: { 'customer.hasTagPrefix': { prefix: 'plan' } } }
conditions: { when: { 'customer.hasAnyTag': { tags: 'vip,premium,whale' } } }
conditions: { when: { 'customer.hasAllTags': { tags: 'active,verified' } } }
```

### Customer Geo Conditions

Route based on the customer's geographic location (detected automatically from IP).

| Condition                | Format   | Params               | Description                                               |
| ------------------------ | -------- | -------------------- | --------------------------------------------------------- |
| `customer.fromCountry`   | `object` | `country` (ISO code) | Customer is from a specific country                       |
| `customer.fromContinent` | `object` | `continent` (code)   | Customer is from a continent (EU, NA, SA, AS, AF, OC, AN) |
| `customer.fromEU`        | `string` | —                    | Customer is from the European Union                       |

```ts theme={null}
conditions: { when: { 'customer.fromCountry': { country: 'US' } } }
conditions: { when: { 'customer.fromContinent': { continent: 'EU' } } }
conditions: { when: 'customer.fromEU' }
```

### Customer Device & Browser Conditions

Route based on the customer's device, browser, or display mode.

| Condition                  | Format   | Params    | Description                                  |
| -------------------------- | -------- | --------- | -------------------------------------------- |
| `customer.onMobile`        | `string` | —         | Customer is on a mobile device               |
| `customer.withBrowser`     | `object` | `browser` | Browser name (chrome, firefox, safari, edge) |
| `customer.withLocale`      | `object` | `locale`  | Browser locale (e.g., en-US, fr-FR)          |
| `customer.isChromeFamily`  | `string` | —         | Chrome, Edge, Brave, or Opera                |
| `customer.isStandalonePWA` | `string` | —         | Installed PWA (standalone mode)              |
| `customer.isAppleSilicon`  | `string` | —         | Apple Silicon Mac (M1/M2/M3)                 |

```ts theme={null}
conditions: { when: 'customer.onMobile' }
conditions: { when: { 'customer.withBrowser': { browser: 'safari' } } }
conditions: { when: { 'customer.withLocale': { locale: 'fr-FR' } } }
conditions: { when: 'customer.isChromeFamily' }
```

### Traffic & Bot Conditions

Route based on traffic source or bot detection.

| Condition                | Format   | Params   | Description                                |
| ------------------------ | -------- | -------- | ------------------------------------------ |
| `customer.fromUtmSource` | `object` | `source` | UTM source (facebook, google, email, etc.) |
| `customer.isBot`         | `string` | —        | Detected as a bot or crawler               |

```ts theme={null}
conditions: { when: { 'customer.fromUtmSource': { source: 'facebook' } } }
conditions: { when: 'customer.isBot' }
```

### Generic Conditions

| Condition | Format   | Description                                |
| --------- | -------- | ------------------------------------------ |
| `always`  | `string` | Always evaluates to true — use as fallback |

```ts theme={null}
conditions: { when: 'always' }
```

### Edge Priority

When multiple edges leave the same node, use `priority` to control evaluation order. Higher priority edges are checked first:

```ts theme={null}
edges: [
  {
    id: 'e_vip_path',
    source: 'step_checkout',
    target: 'step_vip_offer',
    conditions: { when: { 'customer.hasTag': { tag: 'vip' } }, priority: 10 },
  },
  {
    id: 'e_default_path',
    source: 'step_checkout',
    target: 'step_standard_offer',
    conditions: { when: 'always', priority: 1 },
  },
]
```

***

## Going Further

### Add an order bump on the checkout step

Order bumps appear **on** the checkout page, before payment. Use `stepConfig.orderBumps` with `mode: 'custom'` to pick specific bumps:

```ts theme={null}
{
  id: 'step_checkout',
  type: 'checkout',
  config: {
    stepConfig: {
      orderBumps: {
        mode: 'custom',
        enabledOfferIds: ['upsell_xxx'],
      },
    },
  },
}
```

### Chain multiple offers

Add more offer steps between checkout and thank you. Each can have independent accept/decline routing:

```
Checkout → Offer A ──(accepted)──→ Offer B → Thank You
                    ──(declined)──→ Offer C ──(accepted)──→ Thank You
                                             ──(declined)──→ Thank You
```

### Geo-based routing

Show different offers based on customer location:

```ts theme={null}
edges: [
  {
    id: 'e_eu_offer',
    source: 'step_checkout',
    target: 'step_eu_offer',
    conditions: { when: 'customer.fromEU', priority: 10 },
  },
  {
    id: 'e_us_offer',
    source: 'step_checkout',
    target: 'step_us_offer',
    conditions: { when: { 'customer.fromCountry': { country: 'US' } }, priority: 10 },
  },
  {
    id: 'e_default_offer',
    source: 'step_checkout',
    target: 'step_global_offer',
    conditions: { when: 'always', priority: 1 },
  },
]
```

### Generate a checkout link

Use checkout sessions to create pre-loaded cart links:

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

console.log('Share this link:', session.redirectUrl);
```

***

## SDK Methods Reference

### Funnels

| Method                              | Description                          |
| ----------------------------------- | ------------------------------------ |
| `tagada.funnels.create(params)`     | Create a funnel with nodes and edges |
| `tagada.funnels.update(id, params)` | Update config and activate routing   |
| `tagada.funnels.retrieve(id)`       | Get a funnel by ID                   |
| `tagada.funnels.list(storeId)`      | List all funnels for a store         |
| `tagada.funnels.del(id, storeId)`   | Delete a funnel and its routes       |

### Checkout Offers

| Method                                 | Description                  |
| -------------------------------------- | ---------------------------- |
| `tagada.checkoutOffers.create(params)` | Create a post-purchase offer |
| `tagada.checkoutOffers.retrieve(id)`   | Get a checkout offer by ID   |
| `tagada.checkoutOffers.list(params)`   | List offers for a store      |
| `tagada.checkoutOffers.del(params)`    | Delete one or more offers    |

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Funnel Orchestrator" icon="diagram-project" href="/developer-tools/funnels/funnel-orchestrator">
    Deep dive into funnel concepts — nodes, edges, routing, analytics
  </Card>

  <Card title="Step Config Guide" icon="sliders" href="/developer-tools/node-sdk/step-config-guide">
    Configure payment methods, pixels, scripts, and order bumps per step
  </Card>

  <Card title="Funnel Pages" icon="paintbrush" href="/developer-tools/node-sdk/custom-checkout">
    Three ways to build pages: native, custom HTML, or Plugin SDK
  </Card>

  <Card title="Merchant Quick Start" icon="rocket" href="/developer-tools/node-sdk/merchant-quickstart">
    Full setup from processor configuration to live checkout
  </Card>
</CardGroup>
