Skip to main content

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 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:
Coded funnel or external step? An external step 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.
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. 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


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):
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

A step

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:

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.
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().
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.

4. Report events

Three names have a meaning for every pixel and are sent as the provider’s standard event: 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.

5. Push the manifest

.env
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.
  • 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 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, and an undeclared event with sdk-unknown-event: the step itself only changes through the manifest.
A coded funnel loads Meta, TikTok, Snapchat, Google Tag Manager and Microsoft Clarity. Pinterest is not available there.

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 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, never surfaced to the visitor.
  • If your site sends a Content-Security-Policy, allow the pixel origins listed in Content-Security-Policy.

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

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