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

# Funnel Navigation

> Send shoppers into a TagadaPay funnel from your own cart

# Funnel Navigation

`tagada.funnel.navigate()` takes the cart from the loaded checkout session, bootstraps an anonymous CMS session, and returns a redirect URL into a specific funnel step. It's the JS equivalent of the WooCommerce / Shopify checkout plugins — one call from your own site, one URL to redirect to.

<Info>
  **When to use it:**

  * The cart lives in your store (Shopify, WooCommerce, PrestaShop, custom) and you want to route the shopper into a TagadaPay marketing funnel.
  * You want the shopper to enter at a specific funnel step (`step_xxx`) rather than the funnel default.

  **When NOT to use it:**

  * You're building a fully self-hosted checkout page — use [Checkout Flow](/developer-tools/headless-sdk/checkout-flow) directly. Do not follow `createSession().checkoutUrl`.
  * You only need a one-shot **Tagada-hosted** checkout link without a funnel — use `tagada.checkout.createSession()` and follow its `checkoutUrl`.
  * You need a self-hosted checkout URL with no funnel — use `tagada.checkout.createSessionUrl()` so the shopper stays on **your** origin.
</Info>

***

## Prerequisites

`accountId` is required on `createHeadlessClient()` — `funnel.navigate()` will throw `TagadaError('missing_account_id')` without it.

A checkout session must be loaded first (`tagada.checkout.loadSession(...)`). The SDK sources the cart, currency, customer email, and promotion code from that session.

<Note>
  **React provider:** `TagadaHeadlessProvider` only forwards `accountId` through the full `config` prop, not the individual named props. Pass `config={{ storeId, accountId, environment }}` if you need funnel navigation in React.
</Note>

***

## Quick Start

<Tabs>
  <Tab title="Vanilla JavaScript">
    ```typescript theme={null}
    import { createHeadlessClient } from '@tagadapay/headless-sdk';

    const tagada = createHeadlessClient({
      storeId: 'store_abc123',
      accountId: 'acc_abc123',
      environment: 'production',
    });

    // 1. Load (or create) a checkout session — populates the cart.
    await tagada.checkout.loadSession(checkoutToken, sessionToken);

    // 2. Get a redirect URL into the funnel.
    const { url } = await tagada.funnel.navigate({
      funnelId: 'fnlv2_abc123',
      stepId:   'step_abc123',
      returnUrl: 'https://merchant.com/thank-you',
    });

    // 3. Send the shopper there.
    window.location.assign(url);
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={null}
    import {
      TagadaHeadlessProvider,
      useHeadlessClient,
    } from '@tagadapay/headless-sdk/react';

    function App() {
      return (
        <TagadaHeadlessProvider
          config={{
            storeId: 'store_abc123',
            accountId: 'acc_abc123',
            environment: 'production',
          }}
        >
          <BuyNowButton />
        </TagadaHeadlessProvider>
      );
    }

    function BuyNowButton() {
      const sdk = useHeadlessClient();

      const handleBuy = async () => {
        await sdk.checkout.loadSession(checkoutToken, sessionToken);
        const { url } = await sdk.funnel.navigate({
          funnelId: 'fnlv2_abc123',
          stepId:   'step_abc123',
          returnUrl: 'https://merchant.com/thank-you',
        });
        window.location.assign(url);
      };

      return <button onClick={handleBuy}>Buy now</button>;
    }
    ```
  </Tab>

  <Tab title="CDN / Script Tag">
    ```html theme={null}
    <script src="https://cdn.jsdelivr.net/npm/@tagadapay/headless-sdk/dist/tagada-headless.min.js"></script>
    <script>
      const tagada = TagadaHeadless.create({
        storeId: 'store_abc123',
        accountId: 'acc_abc123',
        environment: 'production',
      });

      await tagada.checkout.loadSession(checkoutToken, sessionToken);
      const { url } = await tagada.funnel.navigate({
        funnelId: 'fnlv2_abc123',
        stepId:   'step_abc123',
        returnUrl: 'https://merchant.com/thank-you',
      });

      window.location.assign(url);
    </script>
    ```
  </Tab>
</Tabs>

***

## What Happens Under the Hood

`navigate()` runs the same sequence the WooCommerce plugin does, with caching so repeat calls in the same page are cheap:

1. `POST /api/v1/cms/session/anonymous` — creates an anonymous CMS session, returns a token.
2. `POST /api/v1/cms/session/v2/init` — initializes the CMS session and binds a CMS customer id.
3. `POST /api/v1/funnel/initialize` — creates the funnel session for `funnelId` + `stepId`.
4. `POST /api/v1/funnel/navigate` — fires an `INIT_CHECKOUT` event with the cart and returns `{ url }`.

The CMS token, session id, and customer id are cached on the module instance. Calling `navigate()` again in the same page skips steps 1–3.

***

## `navigate()` Input Reference

| Field              | Type                             | Required | Notes                                                                                                                                       |
| ------------------ | -------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `funnelId`         | `string`                         | yes      | Funnel to enter (e.g. `fnlv2_xxx`).                                                                                                         |
| `stepId`           | `string`                         | yes      | Funnel step to land on. Used for both `entryStepId` and `funnelStepId`.                                                                     |
| `returnUrl`        | `string`                         | no       | Where the funnel should send the shopper back to after checkout.                                                                            |
| `customer`         | `{ email?, currency?, locale? }` | no       | Overrides. Defaults to `session.customer.email` / `session.currency`.                                                                       |
| `discountCodes`    | `string[]`                       | no       | Defaults to `[session.promotionCode]` if a promo is applied, otherwise none.                                                                |
| `metadata`         | `Record<string, unknown>`        | no       | Arbitrary metadata forwarded to the funnel-navigate event and stored on the checkout session. Reserved key: `lockShippingRate` (see below). |
| `externalShipping` | `{ amount, currency }`           | no       | Shipping your storefront already settled on, in minor units. See below.                                                                     |
| `currentUrl`       | `string`                         | no       | Page URL recorded by the funnel. Defaults to `globalThis.location?.href` in browsers.                                                       |
| `accountId`        | `string`                         | no       | Per-call override. Falls back to `HeadlessConfig.accountId`.                                                                                |

***

## Carrying Your Own Shipping Amount

If your storefront already charges its own shipping, pass `externalShipping` so the
shopper is charged the amount they agreed to instead of whatever rate the platform
would auto-select:

```ts theme={null}
await sdk.funnel.navigate({
  funnelId: 'fnlv2_xxx',
  stepId: 'step_xxx',
  externalShipping: { amount: 1200, currency: 'USD' }, // $12.00
});
```

The checkout session starts on a rate matching that exact amount — an existing
rate if one matches, otherwise a dynamically created one.

<Warning>
  Only send this once shipping is genuinely resolved. `{ amount: 0 }` means **free
  shipping** and pins a free rate — it is not a stand-in for "not known yet". While
  shipping is unresolved, omit the field entirely and the platform selects a rate as
  before.
</Warning>

### Keeping your rate when the checkout re-evaluates shipping

The hosted checkout re-evaluates shipping rates when the page loads, when the
shopper changes country, and when discount codes move the cart across a rate's
amount bracket. If the rate your storefront picked stops matching the store's
rate conditions — for example a "$10 under $150" rate on a cart whose catalogue
price is \$170 before the discount — the checkout swaps it for an eligible one.

If your storefront is the authority on shipping, opt out of that re-selection by
sending `lockShippingRate: true` in `metadata` alongside your pre-selected rate:

```ts theme={null}
await sdk.funnel.navigate({
  funnelId: 'fnlv2_xxx',
  stepId: 'step_xxx',
  externalShipping: { amount: 1000, currency: 'USD' }, // $10.00
  metadata: { lockShippingRate: true },
});
```

With the lock set:

* The checkout keeps the pre-selected rate on load, on country changes and on
  discount changes; it never auto-selects another one.
* The shipping selector shows that single rate and the shopper cannot change it.
* The lock only applies once a rate is actually on the session (`externalShipping`,
  or a later `setShippingRate` call). With no rate yet, the checkout behaves as if
  the flag were absent.

Leave the flag off when you want the platform's own rate rules — brackets,
country restrictions, highlighted rate — to drive the selection.

***

## Return Value

```ts theme={null}
{
  url: string;             // Redirect destination.
  funnelSessionId: string; // From /funnel/initialize, useful for logging/debugging.
}
```

The SDK does not perform the redirect — call `window.location.assign(url)`, `router.push(url)`, or whatever your framework uses.

***

## Errors

<Note>
  `funnel.navigate()` throws typed `TagadaError`s:

  * **`no_cart_loaded`** — `sdk.checkout.loadSession(...)` was not called first. The SDK has no cart to send.
  * **`missing_account_id`** — `accountId` was not provided on `createHeadlessClient()` and not passed per call.

  Network and validation failures throw the standard `TagadaNetworkError` / `TagadaValidationError`.
</Note>

***

## SSR & Non-Browser Environments

The SDK reads `globalThis.location?.href` for `currentUrl` when running in a browser. In SSR / Node contexts, pass `currentUrl` explicitly:

```ts theme={null}
await sdk.funnel.navigate({
  funnelId,
  stepId,
  currentUrl: 'https://merchant.com/products/foo',
  returnUrl: 'https://merchant.com/thank-you',
});
```

Because `navigate()` returns `{ url }` rather than redirecting, it composes cleanly with SPA routers (`react-router`, `next/router`) and server-side handoffs.

***

<Tip>
  **Funnel navigation vs. `checkout.createSessionUrl()`:** Use `funnel.navigate()` to enter a marketing funnel (pre-checkout steps, upsell sequencing). Use `checkout.createSessionUrl()` when you just need a self-hosted checkout URL with no funnel logic in front of it.
</Tip>
