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

# Coded Funnels

> Declare the steps of a funnel you code yourself, see them as cards on the canvas, and let TagadaPay fire your pixels on them

# Coded Funnels

A **coded funnel** is a funnel whose pages you build yourself — a React app, a Next.js site, a page generated by Claude Code or Lovable — with the Headless SDK doing the checkout. You describe its steps once in a file, `tagada.funnel.ts`, and push it. Each step then appears as a card on the [canvas](/developer-tools/funnels/canvas) of your store, next to the pages TagadaPay hosts, and you bind your pixels (Meta, TikTok, Snapchat, Google Tag Manager, Microsoft Clarity) to its events from there. Your code reports where the visitor is with one call per step; TagadaPay loads the pixels and fires them.

Three pieces work together:

| Piece | Package | What it does |
| - | - | - |
| `tagada.funnel.ts` + `npx tagada funnel push` | `@tagadapay/node-sdk` | Declares the funnel and its steps. Each step becomes a card on the canvas. |
| `tagada.track` / `useStep` | `@tagadapay/headless-sdk` | Runs in the browser: reports the step the visitor is on and the events that happen there, and fires the pixels bound to them. |
| The step card on the canvas | CRM | Where you choose which pixel hears which event. The card is read-only otherwise: the code owns the step. |

<Info>
  **Coded funnel or external step?** An [external step](/developer-tools/node-sdk/external-steps) is one page of yours added to a funnel TagadaPay hosts, reported from your server or with `curl`. A coded funnel is the whole journey in your code, declared by a manifest, with pixels fired in the browser by the SDK. They are two separate mechanisms and do not share cards.
</Info>

**Requirements**

* `@tagadapay/headless-sdk` **1.16.0** or later (1.15.0 introduced `track` but was never published to npm); **1.18.0** for the React provider form shown below.
* `@tagadapay/node-sdk` **3.23.0** or later, as a dev dependency: it carries `defineFunnel` and the `tagada` command. **3.26.0** or later reads your credentials from `.env` files and prints the pixel origins in `funnel csp`.
* A secret API key and your store id — see [Get an API key](/developer-tools/node-sdk/get-api-key). The key is only used by the command line, never in the browser.
* Node.js 22.18 or later to load a `.ts` manifest. On an older Node, name the file `tagada.funnel.mjs` or write `tagada.funnel.json`.

***

## 1. Install

```bash theme={null}
npm install @tagadapay/headless-sdk
npm install --save-dev @tagadapay/node-sdk
```

***

## 2. Declare the funnel

Create `tagada.funnel.ts` at the root of your project (the command looks for `tagada.funnel.ts`, `.mjs`, `.js`, then `.json` in the current directory):

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

export default defineFunnel({
  key: 'main',
  name: 'Main funnel',
  domains: ['shop.example.com', 'www.shop.example.com'],
  steps: [
    { id: 'landing', type: 'page', name: 'Landing', path: '/', image: 'shots/landing.png', file: 'src/routes/index.tsx' },
    {
      id: 'quiz',
      type: 'page',
      name: 'Quiz',
      path: '/quiz',
      events: [{ name: 'quiz_completed', title: 'Quiz completed', variables: { goal: 'string' } }],
    },
    { id: 'email', type: 'lead', name: 'Email capture', path: '/email' },
    { id: 'product', type: 'product', name: 'Product', path: '/products/:handle' },
    { id: 'checkout', type: 'checkout', name: 'Checkout', path: '/checkout' },
    { id: 'thanks', type: 'thankyou', name: 'Thank you', path: '/thank-you' },
  ],
});
```

`defineFunnel` checks the whole manifest and throws one `TagadaValidationError` that lists every problem with its path (`steps[2].events[0].name: …`). `validateManifest` runs the same checks and returns `{ ok, errors }` instead of throwing.

### The funnel

| Field | Required | What it is |
| - | - | - |
| `key` | yes | The funnel's id within the store. Pass the same value as the `funnel` option of the Headless SDK client. A store can have several coded funnels. |
| `name` | yes | The name shown on the canvas. |
| `domains` | yes | The hosts the funnel runs on: `shop.example.com`, no scheme, no path, no port. Events from a host that is not listed are ignored. `localhost` and `127.0.0.1` are always accepted and must not be listed; an empty array means "local only". |
| `steps` | yes | The steps, in the order the canvas draws them. At least one. |

### A step

| Field | Required | What it is |
| - | - | - |
| `id` | yes | The id your code passes to `track.step()`. It is how TagadaPay matches a visit to a card. |
| `type` | yes | `page`, `product`, `cart`, `lead`, `checkout`, `thankyou` or `portal`. The type decides which pixel events the step offers by default (table below). |
| `name` | yes | The card's title. |
| `path` | yes | The route, starting with `/`. A dynamic route is one step: `/products/:handle`. Shown on the card only — a visit is matched by `id`, never by URL. |
| `next` | no | The ids of the steps that follow. Omitted, the next step in the array; the last step has none. The literal `'@hosted'` means "continues into the hosted TagadaPay funnel of this canvas". |
| `events` | no | The custom events this step reports, each `{ name, title?, variables? }`. `variables` maps a property name to `'string'`, `'number'` or `'boolean'`. |
| `image` | no | A screenshot for the card: a local `.png`, `.jpg`/`.jpeg` or `.webp` file (uploaded by `funnel push`, 3 MB max) or an `https://` URL. |
| `file` | no | The source file of the step, shown on the card so a teammate knows where to look. |

### Step types and their default events

Every step has a **Page view** event. The type adds the standard events a step of that kind usually has:

| `type` | Events the card offers by default |
| - | - |
| `page`, `thankyou`, `portal` | Page view |
| `lead` | Page view, Lead |
| `product` | Page view, ViewContent, AddToCart |
| `cart` | Page view, AddToCart |
| `checkout` | Page view, InitiateCheckout, Purchase |

### Names and limits

* `key`, step `id` and event `name`: lowercase letters, digits, `-` and `_`, starting with a letter or a digit, 64 characters max (`/^[a-z0-9][a-z0-9_-]{0,63}$/`).
* Reserved event names, refused because TagadaPay emits them itself: `page_view`, `checkout_started`, `purchase`, `checkoutStarted`, `cartAbandoned`, `paid`, `paymentFailed`, `orderConfirmed`, and anything starting with `checkout/`, `order/`, `payment/`, `subscription/` or `club/`.
* 60 steps per funnel, 12 events per step, 20 variables per event, 20 domains, names up to 120 characters, `path` up to 512 characters.

***

## 3. Report the steps

Pass the funnel `key` to the client, then call `track.step()` once when the visitor reaches a step.

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

    export const tagada = createHeadlessClient({
      storeId: 'store_abc123',
      funnel: 'main', // the manifest key; defaults to 'main'
    });

    // On the quiz page:
    tagada.track.step('quiz');
    ```
  </Tab>

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

    function App() {
      return (
        <TagadaHeadlessProvider config={{ storeId: 'store_abc123', funnel: 'main' }}>
          <QuizPage />
        </TagadaHeadlessProvider>
      );
    }

    function QuizPage() {
      useStep('quiz');
      return <Quiz />;
    }
    ```

    The provider has no `funnel` prop: pass it inside `config`, which replaces the individual props, `storeId` included. The client is rebuilt only when a value of `config` changes, so an inline object is fine.
  </Tab>
</Tabs>

What `track.step()` does:

* It fires the **Page view** of every pixel bound to that step. `path` defaults to `location.pathname`; pass `track.step('product', { path: '/products/blue-hoodie' })` to report another one.
* Two calls for the same step and path within one second count once, so React StrictMode and two components of the same screen calling `useStep('quiz')` are safe.
* Every later `track.event()` belongs to this step, until the next `track.step()`.

<Warning>
  **Use one client per page.** The checkout session and the tracking state live inside the client. If you create your own client with `createHeadlessClient`, do not also mount `TagadaHeadlessProvider`: it builds a second client, which loads the tracking configuration twice and does not see your checkout session. With your own client, call `tagada.track.step(id)` in a `useEffect` instead of using `useStep`.
</Warning>

***

## 4. Report events

```typescript theme={null}
tagada.track.event('quiz_completed', { goal: 'sleep' });
tagada.track.event('lead', { email });
```

Three names have a meaning for every pixel and are sent as the provider's standard event:

| SDK name | Standard pixel event |
| - | - |
| `lead` | `Lead` |
| `product_viewed` | `ViewContent` |
| `added_to_cart` | `AddToCart` |

Any other name is a **custom event**: declare it in the step's `events`, then bind it on the canvas. A custom event reaches no pixel until you bind it.

A step only offers its Page view, the default events of its type and the events it declares. To send `lead` from a `page` step, for example, add `{ name: 'lead' }` to that step's `events`; an event the step does not offer reaches no pixel.

* The properties must be flat: strings, numbers, booleans. A nested object or array is dropped, not stringified.
* An event sent before the first `track.step()` of the page belongs to no step and is dropped.
* Do not report `InitiateCheckout` or `Purchase` yourself — see [Checkout and purchase](#checkout-and-purchase).

***

## 5. Push the manifest

```bash .env theme={null}
# or .env.local, at the root of your project — never shipped to the browser
TAGADA_API_KEY=sk_...
TAGADA_STORE_ID=store_abc123
```

```bash theme={null}
npx tagada funnel push
```

```
created   landing
created   quiz
created   email
created   product
created   checkout
created   thanks
```

The command validates the manifest, declares the funnel, uploads the local screenshots, and prints one line per step: `created`, `updated`, `unchanged` or `removed`, then any warning.

| Flag | Default | What it does |
| - | - | - |
| `--api-key=` | `TAGADA_API_KEY` | The secret API key. |
| `--store=` | `TAGADA_STORE_ID` | The store id. |
| `--base-url=` | production | Another TagadaPay host, for example a staging environment. |
| `--dry-run` | — | Prints the normalized manifest and its hash, sends nothing, needs no key. |

* Pushing an unchanged manifest does nothing. Push again whenever you add, rename or remove a step.
* A step you remove from the manifest disappears from the canvas. If you add it back with the same `id`, it comes back with the pixels you had bound to it.
* The key and the store id come from the flags first, then from environment variables, then from `.env.local`, then from `.env` in the current directory. Nothing is ever written to those files.
* `npx tagada <your email>` creates a sandbox account if you do not have one yet and writes `TAGADA_API_KEY` and `TAGADA_STORE_ID` to `.env`, so `funnel push` works right after it.

From code, the same calls are on the Node SDK client: `tagada.funnels.declare(storeId, manifest)`, `tagada.funnels.uploadStepImage(storeId, key, stepId, { base64, contentType })` and `tagada.funnels.health(storeId, key?)`. `defineFunnel`, `validateManifest` and the CSP helpers are also exported from `@tagadapay/node-sdk/edge`, which has no Node.js import and runs on Cloudflare Workers, Vercel Edge and Deno.

***

## 6. Bind your pixels on the canvas

Open the canvas of the store: the steps are there as cards with an **SDK** badge, in the order of the manifest, wired by `next`. A step nobody has reached yet carries a **Never seen** pill.

**On the first push, TagadaPay binds the default events for you.** For every pixel integration the store has enabled (Meta, TikTok, Snapchat, Google Tag Manager, Microsoft Clarity), each new step gets its Page view and the default events of its type. This happens once per step: an event you unbind stays unbound on every later push. Custom events are never bound automatically. If the store has no pixel integration yet, nothing is bound, and the first push after you connect one does it.

To change what fires, open a step card and switch pixels on or off per event. Through the API this is a [`bind`](/developer-tools/funnels/canvas#bind) command whose `target` is `{ "kind": "node", "nodeId": "<the card id>" }` and whose `event` is one the step offers (`PageView`, a default event of its type, or one it declares — a declared `lead` binds as `Lead`). Any other command on an SDK card is refused with [`sdk-node-read-only`](/developer-tools/funnels/canvas#sdk-node-read-only), and an undeclared event with [`sdk-unknown-event`](/developer-tools/funnels/canvas#sdk-unknown-event): the step itself only changes through the manifest.

<Note>
  A coded funnel loads Meta, TikTok, Snapchat, Google Tag Manager and Microsoft Clarity. Pinterest is not available there.
</Note>

***

## Checkout and purchase

You never send these two yourself. Once tracking is on (after a first `track.step()`), the SDK fires them from its own checkout and payment modules:

* **InitiateCheckout** when `checkout.loadSession()` loads a checkout session, with the session's amount, currency and items.
* **Purchase** when a payment made through the SDK reaches `succeeded`, directly or after a 3DS redirect. A payment that is only authorized is not a purchase and fires nothing.

Both carry the same event id as the server-side events TagadaPay sends for that checkout and that order (`InitiateCheckout_<checkoutSessionId>`, `Purchase_<orderId>`), so Meta and TikTok count each one once. They fire on the pixels bound to InitiateCheckout and Purchase on the checkout step.

If your checkout step continues into a hosted TagadaPay page (`next: ['@hosted']`), the SDK does not fire Purchase: the hosted thank-you page fires it, and Snapchat, Google Tag Manager and Clarity have no event id to deduplicate a second one.

***

## Identify and consent

```typescript theme={null}
tagada.track.identify({ email, phone, customerId });
tagada.track.consent({ marketing: false });
```

* `identify` hands the visitor's email, phone and TagadaPay customer id to the pixels for their advanced matching.
* Pixels fire without waiting for consent. `consent({ marketing: false })` stops them for the rest of the page and drops the events still waiting; a later `consent({ marketing: true })` on the same page does not turn them back on. Call it as soon as your consent banner knows the answer.

***

## What changes when you upgrade

Nothing, until you call `track.step()`. The SDK makes no tracking request and loads no pixel before that first call, so a site that upgrades without touching its code behaves exactly as before.

* The first `track.step()` turns tracking on for this browser and this store (a `__tgd_track` entry in `localStorage`). From then on, the checkout and purchase pixels above fire on later page loads too.
* Every tracking method returns at once on the server, so server-side rendering is safe.
* The pixel code is not in the main bundle: it is loaded on first use. With the CDN script tag, it is a second file, `tagada-pixels.min.js`, fetched from jsDelivr.
* Tracking never throws. A blocked pixel or a failed request is recorded and reported in [SDK health](/developer-tools/headless-sdk/sdk-health), never surfaced to the visitor.
* If your site sends a Content-Security-Policy, allow the pixel origins listed in [Content-Security-Policy](/developer-tools/headless-sdk/csp).

***

## Check that it works

In the browser, `await tagada.track.health()` tells you what this page did: whether tracking is on, the funnel key, the pixels loaded, and what was blocked.

```typescript theme={null}
const health = await tagada.track.health();
// { enabled: true, storeId: 'store_abc123', funnelKey: 'main',
//   sdk: { name: 'headless', version: '1.17.0' },
//   manifestHash: '…', pixels: { loaded: true, providers: ['facebook', 'tiktok'] }, signals: [] }
```

From your terminal, `npx tagada funnel check` tells you what TagadaPay has seen from every visitor: steps never reached, events never received, pixels blocked, an outdated SDK, a wallet domain not registered. Each check is explained on [SDK health](/developer-tools/headless-sdk/sdk-health).

***

## The endpoints behind it

The SDKs call these routes; you do not need to call them yourself. They are listed so you know what leaves your site. They are not in the API Reference tab.

| Route | Called by | Auth | What it does |
| - | - | - | - |
| `PUT /api/public/v1/sdk/funnels/{key}` | `funnel push`, `funnels.declare()` | Secret API key | Declares the funnel and its steps. |
| `PUT /api/public/v1/sdk/funnels/{key}/steps/{stepId}/image` | `funnel push`, `funnels.uploadStepImage()` | Secret API key | Uploads a step screenshot (base64 JSON). |
| `GET /api/public/v1/sdk/health?storeId=&funnel=` | `funnel check`, `funnels.health()` | Secret API key | The health report. |
| `GET /api/public/v1/sdk/config?storeId=&funnel=` | The browser, once per page | None | Which pixels to load and which events of each step they hear. Cached 60 seconds. |
| `POST /api/public/v1/sdk/events` | The browser (`sendBeacon`) | None | Which steps were seen, which events arrived, which pixels were blocked. |

The two browser routes take no API key and no custom header, so they work from any domain without a CORS preflight. They only accept data for hosts listed in `domains` (plus `localhost`), never carry a payment, and answer the same way for a store that does not exist. A request to them that carries an `Authorization` header is reported in SDK health as a secret key used from a browser.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.