Funnel Pages
A funnel is a sequence of pages (steps). Each page has a type that determines its capabilities — whether it can accept payments, show upsells, fire conversion events, etc.
Page Types
Every node in a funnel has a type. Here are all the available types and what they unlock:
Type Purpose Payment Form Order Bumps Upsell Offers Can be Entry Conversion checkoutPayment page Yes Yes — Yes — thankyouOrder confirmation — — — — Yes offerUpsell / downsell / cross-sell Optional — Yes — — landingLanding page / hero with CTA — — — Yes — customAny custom page Optional Optional Optional Yes — errorError state page — — — — — externalRedirect to external URL — — — Yes — storefront_shopifyShopify product page — — — Yes — storefront_woocommerceWooCommerce product page — — — Yes —
Why types matter
The step type controls what stepConfig options are available:
checkout steps can have paymentSetupConfig (card, Apple Pay, Google Pay routing), orderBumps, and payment config
offer steps can have upsellOffers for post-purchase offers
thankyou steps are marked as the conversion endpoint — analytics events like Purchase fire here
landing and custom steps are general-purpose — they have pixels and scripts but no payment-specific config
// Checkout step — full payment config
{
id : 'step_checkout' ,
type : 'checkout' ,
config : {
stepConfig : {
paymentSetupConfig : { card : { enabled : true , paymentFlowId : 'flow_xxx' } },
orderBumps : { mode : 'custom' , enabledOfferIds : [ 'offer_1' ] },
pixels : { facebook : [ ... ] },
},
},
}
// Offer step — upsell config
{
id : 'step_upsell' ,
type : 'offer' ,
config : {
stepConfig : {
upsellOffers : [{ offerId: 'offer_2' , type: 'one_click' }],
pixels : { facebook : [ ... ] },
},
},
}
// Landing step — pixels and scripts only
{
id : 'step_landing' ,
type : 'landing' ,
isEntry : true ,
config : {
stepConfig : {
pixels : { facebook : [ ... ] },
scripts : [{ name: 'Hotjar' , content: '...' , position: 'head-end' , enabled: true }],
},
},
}
Common funnel patterns
Landing → Checkout → Thank You (simple)
Landing → Checkout → Offer → Thank You (with upsell)
Storefront (Shopify) → Checkout → Thank You (Shopify integration)
Checkout → Offer (EU) / Offer (US) → Thank You (geo-conditional)
Landing → Checkout → Offer → Offer → Thank You (multi-offer)
You can combine any types in any order. The only requirement is that a funnel with payments needs at least one checkout step, and a complete flow should end with a thankyou step for conversion tracking.
Three Approaches
TagadaPay offers three ways to create funnel pages, depending on how much control you need:
Native Checkout Custom HTML Plugin SDK Setup Zero code — auto-injected Deploy raw HTML/CSS/JS Build a React or JS app Page types checkout + thankyou onlyAny type Any type Control TagadaPay handles the UI Full control, no framework Full control + SDK hooks Best for Getting live fast Quick prototypes, simple pages Production-grade branded experiences Payments Automatic You wire up tokenization SDK handles tokenization, 3DS, payments Funnels Automatic navigation Manual routing SDK handles step navigation Deployment 2 steps 4 steps (inline deploy via API) CLI: tgd deploy from your repo A/B testing No Via API split config Via API or CLI
Native Checkout (Recommended) When you create a funnel with checkout and thankyou step types, TagadaPay automatically injects its built-in checkout UI. No plugin deployment, no HTML — just define the flow. Create and activate import Tagada from '@tagadapay/node-sdk' ;
const tagada = new Tagada ( 'your-api-key' );
// Create funnel — native checkout is auto-injected
const funnel = await tagada . funnels . create ({
storeId: 'store_...' ,
config: {
id: 'my-checkout' ,
name: 'My Checkout' ,
version: '1.0.0' ,
nodes: [
{
id: 'step_checkout' ,
name: 'Checkout' ,
type: 'checkout' ,
kind: 'step' ,
isEntry: true ,
position: { x: 0 , y: 0 },
config: { path: '/checkout' },
},
{
id: 'step_thankyou' ,
name: 'Thank You' ,
type: 'thankyou' ,
kind: 'step' ,
position: { x: 300 , y: 0 },
config: { path: '/thankyou' },
},
],
edges: [
{ id: 'e1' , source: 'step_checkout' , target: 'step_thankyou' },
],
},
isDefault: true ,
});
// Activate — triggers routing, mounts pages to CDN
const result = await tagada . funnels . update ( funnel . id , {
storeId: 'store_...' ,
config: funnel . config ,
});
const funnelCheckoutUrl = result . funnel . config . nodes
. find ( n => n . id === 'step_checkout' ). config . url ;
console . log ( 'Funnel live at:' , funnelCheckoutUrl );
// Generate a checkout link with a pre-loaded cart
const session = await tagada . checkout . createSession ({
storeId: 'store_...' ,
items: [{ variantId: 'variant_...' , quantity: 1 }],
currency: 'USD' ,
checkoutUrl: funnelCheckoutUrl ,
});
console . log ( 'Checkout link:' , session . redirectUrl );
No pluginId or instanceId needed. TagadaPay detects that checkout and thankyou nodes have no plugin assigned and injects the native checkout automatically. The checkoutUrl parameter in createSession is critical — it tells TagadaPay to redirect customers to your funnel’s deployed page instead of the default CMS URL.
What you get The native checkout includes:
Card payment form with real-time validation
Order summary with product details and pricing
Multi-currency support based on your store config
Mobile-responsive layout
Built-in thank you page with order confirmation
For the full setup including processor, store, and product creation, see the Merchant Quick Start . Custom HTML Checkout Deploy your own checkout and thank you pages as raw HTML. You have full control over the design, layout, and behavior. Step 1: Define your pages Checkout page — a styled payment form:<! DOCTYPE html >
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Checkout </ title >
< style >
* { box-sizing : border-box ; margin : 0 ; padding : 0 ; }
body { font-family : -apple-system , BlinkMacSystemFont, 'Segoe UI' , sans-serif ; background : #f5f5f7 ; color : #1d1d1f ; }
.container { max-width : 480 px ; margin : 40 px auto ; padding : 0 20 px ; }
.card { background : #fff ; border-radius : 12 px ; padding : 32 px ; box-shadow : 0 2 px 12 px rgba ( 0 , 0 , 0 , 0.08 ); }
h1 { font-size : 22 px ; font-weight : 600 ; margin-bottom : 24 px ; }
.field { margin-bottom : 16 px ; }
.field label { display : block ; font-size : 13 px ; font-weight : 500 ; color : #6e6e73 ; margin-bottom : 6 px ; }
.field input { width : 100 % ; padding : 12 px ; border : 1 px solid #d2d2d7 ; border-radius : 8 px ; font-size : 15 px ; outline : none ; transition : border-color 0.2 s ; }
.field input :focus { border-color : #0071e3 ; box-shadow : 0 0 0 3 px rgba ( 0 , 113 , 227 , 0.1 ); }
.row { display : flex ; gap : 12 px ; }
.row .field { flex : 1 ; }
.divider { height : 1 px ; background : #e5e5ea ; margin : 24 px 0 ; }
.summary { display : flex ; justify-content : space-between ; font-size : 15 px ; margin-bottom : 8 px ; }
.summary.total { font-weight : 600 ; font-size : 17 px ; margin-top : 12 px ; }
.btn { width : 100 % ; padding : 14 px ; background : #0071e3 ; color : #fff ; border : none ; border-radius : 10 px ; font-size : 16 px ; font-weight : 600 ; cursor : pointer ; margin-top : 24 px ; transition : background 0.2 s ; }
.btn:hover { background : #0077ed ; }
.secure { text-align : center ; font-size : 12 px ; color : #86868b ; margin-top : 16 px ; }
</ style >
</ head >
< body >
< div class = "container" >
< div class = "card" >
< h1 > Checkout </ h1 >
< div class = "field" >
< label > Email </ label >
< input type = "email" placeholder = "you@example.com" >
</ div >
< div class = "field" >
< label > Card number </ label >
< input type = "text" placeholder = "4242 4242 4242 4242" >
</ div >
< div class = "row" >
< div class = "field" >< label > Expiry </ label >< input type = "text" placeholder = "MM / YY" ></ div >
< div class = "field" >< label > CVC </ label >< input type = "text" placeholder = "123" ></ div >
</ div >
< div class = "divider" ></ div >
< div class = "summary" >< span > Premium Plan </ span >< span > $29.99 </ span ></ div >
< div class = "summary" >< span > Tax </ span >< span > $0.00 </ span ></ div >
< div class = "summary total" >< span > Total </ span >< span > $29.99 </ span ></ div >
< button class = "btn" > Pay $29.99 </ button >
< p class = "secure" > Secured by TagadaPay </ p >
</ div >
</ div >
</ body >
</ html >
Thank you page — order confirmation with a green checkmark:<! DOCTYPE html >
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Thank You </ title >
< style >
* { box-sizing : border-box ; margin : 0 ; padding : 0 ; }
body { font-family : -apple-system , BlinkMacSystemFont, 'Segoe UI' , sans-serif ; background : #f5f5f7 ; color : #1d1d1f ; }
.container { max-width : 480 px ; margin : 80 px auto ; padding : 0 20 px ; text-align : center ; }
.card { background : #fff ; border-radius : 12 px ; padding : 48 px 32 px ; box-shadow : 0 2 px 12 px rgba ( 0 , 0 , 0 , 0.08 ); }
.checkmark { width : 64 px ; height : 64 px ; background : #34c759 ; border-radius : 50 % ; display : inline-flex ; align-items : center ; justify-content : center ; margin-bottom : 24 px ; }
.checkmark svg { width : 32 px ; height : 32 px ; }
h1 { font-size : 24 px ; font-weight : 600 ; margin-bottom : 12 px ; }
p { font-size : 15 px ; color : #6e6e73 ; line-height : 1.5 ; }
.order-id { display : inline-block ; background : #f5f5f7 ; padding : 8 px 16 px ; border-radius : 8 px ; font-size : 14 px ; font-weight : 500 ; margin-top : 24 px ; color : #1d1d1f ; }
</ style >
</ head >
< body >
< div class = "container" >
< div class = "card" >
< div class = "checkmark" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "#fff" stroke-width = "3"
stroke-linecap = "round" stroke-linejoin = "round" >
< polyline points = "20 6 9 17 4 12" />
</ svg >
</ div >
< h1 > Thank you! </ h1 >
< p > Your order has been confirmed. < br > You will receive a confirmation email shortly. </ p >
< span class = "order-id" > Order #TGD-10042 </ span >
</ div >
</ div >
</ body >
</ html >
Step 2: Deploy as a plugin Upload both pages as base64-encoded inline assets: import Tagada from '@tagadapay/node-sdk' ;
import { readFileSync } from 'fs' ;
const tagada = new Tagada ( 'your-api-key' );
// If loading from files:
// const checkoutHtml = readFileSync('checkout.html', 'utf-8');
// const thankyouHtml = readFileSync('thankyou.html', 'utf-8');
const deployment = await tagada . plugins . deploy ({
storeId: 'store_...' ,
manifest: {
name: 'my-custom-checkout' ,
description: 'Custom branded checkout' ,
mode: 'spa-mode' ,
version: '1.0.0' ,
pages: [
{ id: 'checkout' , path: '/checkout' , name: 'Checkout' , isDefault: true },
{ id: 'thankyou' , path: '/thankyou' , name: 'Thank You' },
],
},
inlineAssets: [
{ path: 'checkout/index.html' , content: btoa ( checkoutHtml ), contentType: 'text/html' },
{ path: 'thankyou/index.html' , content: btoa ( thankyouHtml ), contentType: 'text/html' },
],
autoInstantiate: {
stepType: 'checkout' ,
isPreview: false ,
},
});
const { pluginId , deploymentId , instanceId } = deployment ;
console . log ( 'Deployed:' , { pluginId , deploymentId , instanceId });
autoInstantiate creates a plugin instance automatically. Without it, you’d need a separate tagada.plugins.instantiate() call.
Step 3: Create a funnel using your plugin Reference pluginId and instanceId in each funnel node: const funnel = await tagada . funnels . create ({
storeId: 'store_...' ,
config: {
id: 'custom-checkout' ,
name: 'Custom Checkout' ,
version: '1.0.0' ,
nodes: [
{
id: 'step_checkout' ,
name: 'Checkout' ,
type: 'checkout' ,
kind: 'step' ,
isEntry: true ,
position: { x: 0 , y: 0 },
config: {
pluginId , instanceId ,
instanceVersion: '1.0.0' ,
variant: 'Default' ,
path: '/checkout' ,
metadata: { internalPath: '/checkout' , mountPoint: 'default' },
},
},
{
id: 'step_thankyou' ,
name: 'Thank You' ,
type: 'thankyou' ,
kind: 'step' ,
position: { x: 300 , y: 0 },
config: {
pluginId , instanceId ,
instanceVersion: '1.0.0' ,
variant: 'Default' ,
path: '/thankyou' ,
metadata: { internalPath: '/thankyou' , mountPoint: 'default' },
},
},
],
edges: [
{ id: 'e1' , source: 'step_checkout' , target: 'step_thankyou' },
],
},
isDefault: true ,
});
Step 4: Activate const result = await tagada . funnels . update ( funnel . id , {
storeId: 'store_...' ,
config: funnel . config ,
});
const url = result . funnel . config . nodes [ 0 ]. config . url ;
console . log ( 'Custom checkout live at:' , url );
Plugin SDK Checkout The Plugin SDK (@tagadapay/plugin-sdk) is the professional approach for teams with a codebase. You build a React (or vanilla JS) SPA that uses TagadaPay’s SDK hooks for session management, card tokenization, 3DS, payment processing, funnel navigation, and analytics — then deploy it from your repo with the CLI. This is different from Custom HTML. With raw HTML you handle everything yourself. With the Plugin SDK, you get React hooks (or a standalone JS API) that connect your UI to TagadaPay’s backend — sessions, checkout data, payments, redirects — out of the box.
What you get from the SDK
useCheckoutSession() — cart items, prices, customer data
useCardTokenization() — PCI-compliant card form, Apple Pay, Google Pay
usePaymentProcessors() — process payments through your PSP routing
useThreeds() — 3DS authentication with automatic challenge handling
useFunnelNavigation() — step-to-step navigation with data passing
useFunnelData() — shared state across funnel steps
Full TypeScript types
Step 1: Scaffold your project npm install -g @tagadapay/plugin-cli
tgd login
tgd init my-checkout --template react-checkout
cd my-checkout
This generates a project with: my-checkout/
├── src/
│ ├── pages/
│ │ ├── checkout/ # Checkout page component
│ │ └── thankyou/ # Thank you page component
│ └── App.tsx
├── plugin.manifest.json # Plugin metadata + page definitions
├── package.json
└── vite.config.ts # Pre-configured build
Step 2: Build your checkout page import {
useCheckoutSession ,
useCardTokenization ,
usePaymentProcessors ,
useFunnelNavigation ,
} from '@tagadapay/plugin-sdk/react' ;
export default function CheckoutPage () {
const { session , cart , totals } = useCheckoutSession ();
const { tokenizeCard , CardForm } = useCardTokenization ();
const { processPayment , isProcessing } = usePaymentProcessors ();
const { navigateToNext } = useFunnelNavigation ();
async function handlePay () {
const { tagadaToken } = await tokenizeCard ();
const result = await processPayment ({ tagadaToken });
if ( result . status === 'succeeded' ) {
navigateToNext ({ orderId: result . orderId });
}
}
return (
< div className = "checkout" >
< h1 > Checkout </ h1 >
{ /* Cart summary — pulled from session automatically */ }
< div className = "order-summary" >
{ cart . items . map ( item => (
< div key = { item . id } >
< span > { item . name } </ span >
< span > { item . formattedPrice } </ span >
</ div >
)) }
< div className = "total" > { totals . formattedTotal } </ div >
</ div >
{ /* PCI-compliant card form — SDK handles tokenization */ }
< CardForm />
< button onClick = { handlePay } disabled = { isProcessing } >
{ isProcessing ? 'Processing...' : `Pay ${ totals . formattedTotal } ` }
</ button >
</ div >
);
}
Step 3: Build the thank you page import { useFunnelData } from '@tagadapay/plugin-sdk/react' ;
export default function ThankYouPage () {
const { orderId } = useFunnelData ();
return (
< div className = "thankyou" >
< h1 > Thank you! </ h1 >
< p > Your order < strong > { orderId } </ strong > has been confirmed. </ p >
</ div >
);
}
{
"name" : "my-branded-checkout" ,
"description" : "Production checkout with React" ,
"version" : "1.0.0" ,
"mode" : "spa-mode" ,
"pages" : [
{ "id" : "checkout" , "path" : "/checkout" , "name" : "Checkout" , "isDefault" : true },
{ "id" : "thankyou" , "path" : "/thankyou" , "name" : "Thank You" }
]
}
Step 5: Deploy npm run build
tgd deploy --store store_xxx
✅ Deployed my-branded-checkout@1.0.0
Plugin: plugin_abc123
Instance: inst_xyz789
Checkout: https://my-branded-checkout--store_xxx.cdn.tagadapay.com/checkout
Thank You: https://my-branded-checkout--store_xxx.cdn.tagadapay.com/thankyou
Step 6: Create a funnel and activate Use the returned pluginId and instanceId from deployment: import Tagada from '@tagadapay/node-sdk' ;
const tagada = new Tagada ( 'your-api-key' );
const funnel = await tagada . funnels . create ({
storeId: 'store_xxx' ,
config: {
id: 'branded-checkout' ,
name: 'Branded Checkout' ,
version: '1.0.0' ,
nodes: [
{
id: 'step_checkout' ,
name: 'Checkout' ,
type: 'checkout' ,
kind: 'step' ,
isEntry: true ,
position: { x: 0 , y: 0 },
config: {
pluginId: 'plugin_abc123' ,
instanceId: 'inst_xyz789' ,
instanceVersion: '1.0.0' ,
variant: 'Default' ,
path: '/checkout' ,
metadata: { internalPath: '/checkout' , mountPoint: 'default' },
},
},
{
id: 'step_thankyou' ,
name: 'Thank You' ,
type: 'thankyou' ,
kind: 'step' ,
position: { x: 300 , y: 0 },
config: {
pluginId: 'plugin_abc123' ,
instanceId: 'inst_xyz789' ,
instanceVersion: '1.0.0' ,
variant: 'Default' ,
path: '/thankyou' ,
metadata: { internalPath: '/thankyou' , mountPoint: 'default' },
},
},
],
edges: [
{ id: 'e1' , source: 'step_checkout' , target: 'step_thankyou' },
],
},
isDefault: true ,
});
const result = await tagada . funnels . update ( funnel . id , {
storeId: 'store_xxx' ,
config: funnel . config ,
});
console . log ( 'Live at:' , result . funnel . config . nodes [ 0 ]. config . url );
Vanilla JS (no React) The Plugin SDK also works without React: import { TagadaPlugin } from '@tagadapay/plugin-sdk' ;
const plugin = new TagadaPlugin ();
await plugin . initialize ();
const session = plugin . getCheckoutSession ();
const tokenizer = plugin . getCardTokenizer ();
document . getElementById ( 'pay-btn' ). addEventListener ( 'click' , async () => {
const { tagadaToken } = await tokenizer . tokenize ({
cardNumber: document . getElementById ( 'card' ). value ,
expiryDate: document . getElementById ( 'expiry' ). value ,
cvc: document . getElementById ( 'cvc' ). value ,
});
const result = await plugin . processPayment ({ tagadaToken });
if ( result . status === 'succeeded' ) {
plugin . navigateToNext ({ orderId: result . orderId });
}
});
Updating a deployed plugin Deploy a new version — zero downtime: # Bump version in plugin.manifest.json
tgd deploy --store store_xxx
The CLI handles versioning and instance creation. Then update the funnel to point to the new instance version.
Updating a Live Plugin
To update your custom checkout without downtime, deploy a new version with the same plugin name:
const updated = await tagada . plugins . deploy ({
storeId: 'store_...' ,
manifest: {
name: 'my-custom-checkout' ,
version: '1.1.0' ,
mode: 'spa-mode' ,
pages: [
{ id: 'checkout' , path: '/checkout' , name: 'Checkout' , isDefault: true },
{ id: 'thankyou' , path: '/thankyou' , name: 'Thank You' },
],
},
inlineAssets: [ /* updated HTML/CSS/JS */ ],
autoInstantiate: { stepType: 'checkout' , isPreview: false },
});
// Point the funnel to the new instance
const config = JSON . parse ( JSON . stringify ( funnel . config ));
for ( const node of config . nodes ) {
node . config . instanceId = updated . instanceId ;
node . config . instanceVersion = '1.1.0' ;
}
await tagada . funnels . update ( funnel . id , {
storeId: 'store_...' ,
config ,
});
A/B Testing
Split traffic between two checkout designs:
const variantB = await tagada . plugins . deploy ({
storeId: 'store_...' ,
manifest: {
name: 'checkout-variant-b' ,
version: '1.0.0' ,
mode: 'spa-mode' ,
pages: [
{ id: 'checkout' , path: '/checkout' , name: 'Checkout B' , isDefault: true },
],
},
inlineAssets: [ /* variant B HTML */ ],
autoInstantiate: { stepType: 'checkout' , isPreview: false },
});
// Get routeId from the activated funnel
const checkoutNode = result . funnel . config . nodes
. find ( n => n . id === 'step_checkout' );
const routeId = checkoutNode . config . routeId ;
// 50/50 split
await fetch ( ` ${ baseUrl } /plugins/v2/split` , {
method: 'POST' ,
headers: { Authorization: `Bearer ${ apiKey } ` , 'Content-Type' : 'application/json' },
body: JSON . stringify ({
routeId ,
storeId: 'store_...' ,
splitConfig: {
type: 'weighted' ,
instances: [
{ instanceId: deployment . instanceId , deploymentId: deployment . deploymentId , weight: 50 },
{ instanceId: variantB . instanceId , deploymentId: variantB . deploymentId , weight: 50 },
],
stickyDuration: 3600 ,
},
}),
});
Direct Mounting (Without Funnel)
For standalone pages (landing pages, portals) outside of a checkout flow:
await tagada . plugins . mount ({
deploymentId ,
deploymentInstanceId: instanceId ,
storeId: 'store_...' ,
alias: 'my-landing-page' ,
matcher: '.*' ,
force: true ,
});
// → my-landing-page--store_xxx.cdn.tagadapay.com
Plugin Management
const plugins = await tagada . plugins . list ( 'store_...' );
const instances = await tagada . plugins . listInstances ( 'store_...' );
const details = await tagada . plugins . deploymentDetails ( deploymentId , 'store_...' );
await tagada . plugins . del ( pluginId , 'store_...' );
Next Steps
Plugin SDK Reference Full SDK documentation — hooks, API, examples
Plugin CLI Deploy plugins from your terminal with tgd deploy
Merchant Quick Start Full 7-step setup from processor to live checkout
Funnel Orchestrator Multi-step funnels with routing and analytics