> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cello.so/llms.txt
> Use this file to discover all available pages before exploring further.

# React + Node.js Integration Guide

> Comprehensive guide to integrating Cello's referral system into React + Node.js applications

# Cello Integration Guide for React + Node.js

A comprehensive guide to integrating Cello's referral system into any React + Node.js application. This guide covers the referral widget, public attribution, signup tracking, and Stripe integration.

***

## Integration Planning Framework

<Warning>
  **IMPORTANT:** When planning a Cello integration, structure your plan into **TWO PHASES**:
</Warning>

### Phase 1: Mandatory Core Integration

**Must be implemented for Cello to work:**

* ✅ Step 1: Server-Side Helpers
* ✅ Step 2: Referral Widget (`cello.js` - Authenticated Users)
* ✅ Step 3: Public Attribution (`cello-attribution.js` - Public Pages)
* ✅ Step 4: Signup Event Tracking
* ✅ Step 5: Stripe Integration

**Critical for Custom Launchers:**

* ✅ Dynamic reward labels (Step 2.3.D) - **REQUIRED if using custom launcher**

### Phase 2: Optional Enhancements

**Highly recommended but can be added later:**

* ⭐ Enhanced error handling & logging
* ⭐ Retry logic for failed operations
* ⭐ Loading states & user feedback
* ⭐ Advanced signup personalization
* ⭐ Referral code validation
* ⭐ Campaign-specific messaging
* ⭐ Environment validation
* ⭐ Detailed debugging tools

### Why Two Phases?

**Phase 1** gets Cello working and generating referrals.\
**Phase 2** makes it production-ready, debuggable, and user-friendly.

<Warning>
  **Common Mistake:** Treating Phase 2 features (especially dynamic labels for custom launchers) as truly optional. They're essential for a good user experience.
</Warning>

**Best Practice:** Plan both phases upfront, then implement Phase 1 fully before moving to Phase 2. This ensures nothing critical is missed.

***

## General Prerequisites

Before starting the Cello integration, ensure your application meets these requirements:

### Application Requirements

| Requirement               | Description                                                          |
| ------------------------- | -------------------------------------------------------------------- |
| **Authentication System** | Your app must have user authentication with session/token management |
| **User Model**            | Users must have unique IDs, email addresses, and names               |
| **Server-Side Rendering** | Support for SSR or hybrid rendering (Next.js, Remix, etc.)           |
| **API Routes**            | Ability to create server-side API endpoints                          |

### Environment Variables

Create these environment variables in your `.env` file:

```bash theme={null}
# Required for Referral Widget
CELLO_PRODUCT_ID=your-product-id          # From Cello Portal
CELLO_PRODUCT_SECRET=your-product-secret  # From Cello Portal (for JWT signing)
CELLO_ENV=sandbox                          # "sandbox" or "production"

# Required for API Calls (Attribution, Event Tracking)
CELLO_ACCESS_KEY_ID=your-access-key-id
CELLO_SECRET_ACCESS_KEY=your-secret-access-key

# Optional
CELLO_API_BASE_URL=https://api.sandbox.cello.so  # Defaults based on CELLO_ENV
```

<Note>
  **Environment variable names:** This guide uses `CELLO_PRODUCT_ID`, `CELLO_PRODUCT_SECRET`, and `CELLO_ENV`. Your project may use different names. Product ID and secret are **server-side only** - do not use `NEXT_PUBLIC_` for them. Use the script URLs from [Quickstart](/referral-component/quickstart) and [Embedded Script Tag](/sdk/client-side/embedded-script-tag) in the steps below.
</Note>

### Cello Portal Setup

1. Create an account at [cello.so](https://cello.so)
2. Create a product and note the `Product ID` and `Product Secret`
3. Generate API access keys for server-side calls at **[portal.cello.so/integrations/accesskeys](https://portal.cello.so/integrations/accesskeys)**

<Warning>
  **Two Separate Credential Pairs:**
  Cello uses two completely different sets of credentials that are easy to confuse:

  | Credential                                        | Used For                                                  | Found In                                                                                     |
  | ------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
  | `CELLO_PRODUCT_ID` + `CELLO_PRODUCT_SECRET`       | Signing JWTs for the referral widget boot config          | Cello Portal → Product Settings                                                              |
  | `CELLO_ACCESS_KEY_ID` + `CELLO_SECRET_ACCESS_KEY` | Server-to-server API calls (token fetch + event emission) | Cello Portal → [Integrations → Access Keys](https://portal.cello.so/integrations/accesskeys) |

  Before implementing any server-side API calls, read the official API reference:

  * **Overview:** [API Reference](/api-reference/introduction)
  * **Get token:** [Authentication](/api-reference/authentication/get-token)
  * **Send event:** [Send Event](/api-reference/generic-events/send-event)
</Warning>

4. Configure your reward structure and campaigns

***

## Cello Scripts Overview

Cello provides two separate JavaScript SDKs for different purposes:

| Script                   | Purpose                                 | Used In | Users                             |
| ------------------------ | --------------------------------------- | ------- | --------------------------------- |
| **cello.js**             | Referral widget for authenticated users | Step 2  | Authenticated users only          |
| **cello-attribution.js** | Track referral codes from URL           | Step 3  | Public visitors (unauthenticated) |

**Key Distinction:**

* `cello.js` - Loaded in **authenticated layouts** to show the referral widget/launcher
* `cello-attribution.js` - Loaded in **public layouts** to capture referral codes from URL parameters

Both scripts are independent and serve different purposes in the referral flow.

***

## Step 1: Server-Side Helpers

### 1.1 Documentation

* [User Authentication](/sdk/client-side/user-authentication) - JWT structure and signing
* [Referral Component Quickstart](/referral-component/quickstart) - Boot configuration

### 1.2 Prerequisites

* `CELLO_PRODUCT_ID` and `CELLO_PRODUCT_SECRET` environment variables set
* A JWT signing library (e.g., `jsonwebtoken` for Node.js)

### 1.3 Integration Description

Create a server-side helper module with three functions:

#### A. JWT Generator

Generate HS512-signed JWTs for widget authentication. **CRITICAL: Use Cello's custom claim names, not standard JWT claims.**

```typescript theme={null}
import jwt from "jsonwebtoken"

export function createCelloJwt(userId: string): string {
  const payload = {
    productId: process.env.CELLO_PRODUCT_ID,  // NOT "iss"
    productUserId: userId,                      // NOT "sub"
    iat: Math.floor(Date.now() / 1000),        // Only these 3 fields
  }
  
  return jwt.sign(payload, process.env.CELLO_PRODUCT_SECRET!, {
    algorithm: "HS512",  // Must be HS512
  })
}
```

**Common Mistakes to Avoid:**

* ❌ Using `iss`, `sub`, `exp` (standard JWT claims)
* ❌ Including `name`, `email` in the JWT
* ✅ Use exactly: `productId`, `productUserId`, `iat`

#### B. Script URL Resolver

Use the same script URLs as in the [Referral Component Quickstart](/referral-component/quickstart) (path includes `/latest/`).

```typescript theme={null}
export function getCelloScriptUrl(): string {
  const env = process.env.CELLO_ENV || "sandbox"
  return env === "production"
    ? "https://assets.cello.so/app/latest/cello.js"
    : "https://assets.sandbox.cello.so/app/latest/cello.js"
}
```

**CRITICAL URLs:**

* ✅ Sandbox: `https://assets.sandbox.cello.so/app/latest/cello.js`
* ✅ Production: `https://assets.cello.so/app/latest/cello.js`
* ❌ Wrong: `https://sandbox.cello.so/widget/cello.js`
* ❌ Wrong: `https://assets.sandbox.cello.so/app/cello.js` (missing `/latest/` → Access Denied)

#### C. Boot Configuration Builder

```typescript theme={null}
export function buildCelloBootConfig(user: { id: string; email: string; name: string }) {
  return {
    productId: process.env.CELLO_PRODUCT_ID,
    token: createCelloJwt(user.id),
    language: "en",
    productUserDetails: {
      firstName: user.name.split(" ")[0] ?? user.name,
      lastName: user.name.split(" ").slice(1).join(" ") || undefined,
      fullName: user.name,
      email: user.email,
    },
  }
}
```

### 1.4 Acceptance Criteria

* [ ] `createCelloJwt()` generates a valid HS512 JWT with `productId`, `productUserId`, `iat` claims only
* [ ] `getCelloScriptUrl()` returns correct URL based on environment
* [ ] `buildCelloBootConfig()` returns config with `productUserDetails` including `firstName`, `lastName`, `fullName`, `email`
* [ ] No sensitive data (product secret) is exposed to the client

***

## Step 2: Referral Widget (Authenticated Users)

**📦 Required Script:** `cello.js` (Referral Widget)

This step integrates the Cello referral widget for **authenticated users only**. The widget allows users to access their referral link, share it, and track referrals.

### 2.1 Documentation

* [Referral Component Quickstart](/referral-component/quickstart)
* [Custom Launcher](/referral-component/custom-launcher)

### 2.2 Prerequisites

* Server-side helpers from Step 1 implemented
* Authenticated user session available
* Authenticated layout/pages where widget will appear

### 2.3 Integration Description

#### A. Create Token API Endpoint

Create an API route that returns boot configuration for authenticated users:

```typescript theme={null}
// /api/cello/token (or equivalent)
export async function GET(request: Request) {
  const user = await getCurrentUser(request)  // Your auth logic
  
  if (!user || !process.env.CELLO_PRODUCT_ID) {
    return Response.json({ enabled: false })
  }
  
  return Response.json({
    enabled: true,
    bootConfig: buildCelloBootConfig(user),
  })
}
```

#### B. Add Scripts to Authenticated Layout (cello.js)

**CRITICAL: Use cello.js for authenticated users**

This script enables the referral widget functionality. Do not confuse with `cello-attribution.js` (used in Step 3 for public pages).

Per [Referral Component Quickstart](/referral-component/quickstart): load the script with `type="module"` and `async`. Use the same tag shape in your layout (raw `<script>` with those attributes). In Next.js, if you use `next/script`, ensure the script is loaded as a module (e.g. pass `type="module"` or use the raw tag below).

In your authenticated layout, add the Cello queue snippet and script:

```tsx theme={null}
// Authenticated layout - script must have type="module" and async (per Quickstart)
export default function AuthenticatedLayout({ children }) {
  const celloScriptUrl = getCelloScriptUrl()
  
  return (
    <html>
      <body>
        {/* Queue snippet - must load before script */}
        <script
          dangerouslySetInnerHTML={{
            __html: `window.cello = window.cello || { cmd: [] };`
          }}
        />
        
        {/* Cello widget script - type="module" and async per docs.cello.so/referral-component/quickstart */}
        <script
          src={celloScriptUrl}
          type="module"
          async
          crossOrigin="anonymous"
        />
        
        <CelloBootstrap />
        {children}
      </body>
    </html>
  )
}
```

**Script attributes (from [Quickstart](/referral-component/quickstart)):** `type="module"`, `async`. Do not add `data-product-id` to the widget script.

#### C. Create Bootstrap Component

First, add TypeScript declarations for the Cello SDK:

```typescript theme={null}
// types/cello.d.ts (or at top of component file)
declare global {
  interface Window {
    cello?: {
      cmd: Array<(cello: CelloSDK) => void | Promise<void>>
    }
    Cello?: (command: string, ...args: unknown[]) => Promise<unknown>
  }
}

interface CelloSDK {
  boot: (config: CelloBootConfig) => Promise<void>
}

interface CelloBootConfig {
  productId: string
  token: string
  language: string
  productUserDetails: {
    firstName: string
    lastName?: string
    fullName: string
    email: string
  }
}
```

Then create the bootstrap component:

```tsx theme={null}
"use client"  // For Next.js App Router

import { useEffect, useState } from "react"

export function CelloBootstrap() {
  const [hasBooted, setHasBooted] = useState(false)

  useEffect(() => {
    if (hasBooted) return
    
    async function bootCello() {
      try {
        const response = await fetch("/api/cello/token", {
          credentials: "include",  // Include session cookies
        })
        const { enabled, bootConfig } = await response.json()
        
        if (!enabled || !bootConfig) return
        
        // Defensive initialization in case queue snippet didn't load
        if (!window.cello) {
          window.cello = { cmd: [] }
        }
        
        // Official boot pattern
        window.cello.cmd.push(async function(cello) {
          await cello.boot(bootConfig)
          console.log("[Cello] Widget booted successfully")
        })
        
        setHasBooted(true)
      } catch (error) {
        console.error("[Cello] Boot failed:", error)
      }
    }
    
    bootCello()
  }, [hasBooted])
  
  return null
}
```

#### D. Custom Launcher

<Warning>
  **CRITICAL - PHASE 1 REQUIREMENT:** If using a custom launcher instead of Cello's default floating button, you **MUST** implement dynamic reward labels. Without them, users won't see the reward amount and won't be motivated to refer.
</Warning>

**Planning Note:** While custom launchers are technically optional (you can use Cello's default floating button), dynamic labels are **mandatory** if you choose to implement a custom launcher. Include this in your Phase 1 plan, not Phase 2.

##### Basic Custom Launcher Button

```tsx theme={null}
export function ReferralLauncherButton() {
  return (
    <button
      className="cello-launcher"  // Required class for Cello to find
      style={{ position: "relative" }}  // Required for badge positioning
    >
      Earn Rewards
    </button>
  )
}
```

##### Dynamic Reward Labels (REQUIRED for Custom Launchers)

The key feature of custom launchers is showing the dynamic reward amount (e.g., "Earn €1000" instead of static "Earn"). Fetch labels after boot:

```typescript theme={null}
window.cello.cmd.push(async function(cello) {
  await cello.boot(config)
  
  // Fetch dynamic labels
  const labels = await window.Cello("getLabels")
  // labels.customLauncher contains "Earn €1000" etc.
  
  // Update your button text with the dynamic label
  updateLauncherText(labels.customLauncher)
})
```

##### Common Timing Issue and Solution

<Warning>
  **Problem:** Dynamic labels don't appear on first login, only after page refresh.
</Warning>

**Root Cause:** This is a state management/timing issue, not a Cello bug. The launcher component renders before labels are fetched, and without proper state management, the component doesn't re-render when labels become available.

**Incorrect Flow:**

1. User logs in
2. Launcher component renders with fallback text
3. Bootstrap fetches labels asynchronously
4. Labels are fetched but component doesn't know about them
5. User refreshes → component re-renders with labels already in state ✓

**Correct Implementation Flow:**

To fix this issue, you need a mechanism to notify components when labels are ready:

1. Boot Cello in bootstrap component
2. After boot completes, fetch labels via `window.Cello("getLabels")`
3. Store labels in shared state (global variable, context, or state management)
4. Notify components that labels are ready (custom event, callback, or state update)
5. Components listen for labels and update UI accordingly

##### Implementation Approaches

**1. Event-Based Pattern (Framework Agnostic)**

Works with any framework or vanilla JavaScript:

```typescript theme={null}
// Bootstrap component - fetch and broadcast labels
"use client"

import { useEffect, useState } from "react"

const CELLO_LABELS_EVENT = "cello:labels:ready"

export function CelloBootstrap() {
  const [hasBooted, setHasBooted] = useState(false)

  useEffect(() => {
    if (hasBooted) return
    
    async function bootCello() {
      try {
        const response = await fetch("/api/cello/token", {
          credentials: "include",
        })
        const { enabled, bootConfig } = await response.json()
        
        if (!enabled || !bootConfig) return
        
        if (!window.cello) {
          window.cello = { cmd: [] }
        }
        
        window.cello.cmd.push(async function(cello) {
          await cello.boot(bootConfig)
          console.log("[Cello] Widget booted successfully")
          
          // CRITICAL: Fetch labels after boot
          const labels = await window.Cello("getLabels")
          console.log("[Cello] Labels fetched:", labels)
          
          // Store globally for immediate access
          window.__celloLabels = labels
          
          // Broadcast event to all components
          window.dispatchEvent(new CustomEvent(CELLO_LABELS_EVENT, {
            detail: labels
          }))
        })
        
        setHasBooted(true)
      } catch (error) {
        console.error("[Cello] Boot failed:", error)
      }
    }
    
    bootCello()
  }, [hasBooted])
  
  return null
}
```

```typescript theme={null}
// Launcher component - listen for labels
"use client"

import { useEffect, useState } from "react"

const CELLO_LABELS_EVENT = "cello:labels:ready"

export function ReferralLauncherButton() {
  const [labelText, setLabelText] = useState("Invite & Earn")  // Fallback
  
  useEffect(() => {
    // Check if labels already available (e.g., after navigation)
    if (window.__celloLabels?.customLauncher) {
      setLabelText(window.__celloLabels.customLauncher)
    }
    
    // Listen for labels event
    const handleLabels = (event: CustomEvent) => {
      const labels = event.detail
      if (labels?.customLauncher) {
        setLabelText(labels.customLauncher)
      }
    }
    
    window.addEventListener(CELLO_LABELS_EVENT, handleLabels)
    return () => window.removeEventListener(CELLO_LABELS_EVENT, handleLabels)
  }, [])
  
  return (
    <button
      className="cello-launcher"
      style={{ position: "relative" }}
    >
      {labelText}
    </button>
  )
}
```

**TypeScript Declarations:**

```typescript theme={null}
// types/cello.d.ts
declare global {
  interface Window {
    __celloLabels?: {
      customLauncher?: string
      // ... other label fields
    }
  }
}
```

**2. React Context Pattern**

For React applications, wrap your authenticated app with a context provider:

```typescript theme={null}
// context/CelloContext.tsx
const CelloLabelsContext = createContext<{ labels: CelloLabels | null }>({ labels: null })

export function CelloLabelsProvider({ children }) {
  const [labels, setLabels] = useState<CelloLabels | null>(null)
  
  useEffect(() => {
    const handleLabels = (event: CustomEvent) => {
      setLabels(event.detail)
    }
    
    window.addEventListener(CELLO_LABELS_EVENT, handleLabels)
    return () => window.removeEventListener(CELLO_LABELS_EVENT, handleLabels)
  }, [])
  
  return (
    <CelloLabelsContext.Provider value={{ labels }}>
      {children}
    </CelloLabelsContext.Provider>
  )
}

export const useCelloLabels = () => useContext(CelloLabelsContext)
```

Then in your launcher:

```typescript theme={null}
export function ReferralLauncherButton() {
  const { labels } = useCelloLabels()
  
  return (
    <button className="cello-launcher" style={{ position: "relative" }}>
      {labels?.customLauncher || "Invite & Earn"}
    </button>
  )
}
```

**3. State Management (Redux/Zustand/etc.)**

Dispatch labels to your global store after boot, then components subscribe to the store.

**Key Principles (Any Approach):**

1. ✅ Always provide fallback text for initial render
2. ✅ Store labels in shared state (global variable, context, store)
3. ✅ Use event/callback/subscription to notify components
4. ✅ Check for existing labels on component mount (handles navigation)
5. ✅ Clean up event listeners on unmount

**Why This Matters:**

* ❌ Static "Earn" - No clear incentive
* ✅ Dynamic "Earn €1000" - Clear value proposition
* ❌ Labels only on refresh - Poor user experience
* ✅ Labels on first login - Professional implementation

### 2.4 Acceptance Criteria

**Phase 1 - Basic Widget Functionality (Mandatory):**

* [ ] Token endpoint returns `{ enabled: true, bootConfig: {...} }` for authenticated users
* [ ] Token endpoint returns `{ enabled: false }` for unauthenticated users
* [ ] Cello script loads without CORS errors in browser Network tab
* [ ] Console shows no "User is not authorized" errors
* [ ] Widget opens when clicking the launcher button
* [ ] Notification badges appear on the launcher (test with your own referral link)

**Phase 1 - Custom Launcher with Dynamic Labels (REQUIRED If Using Custom Launcher):**

* [ ] **Dynamic reward label displays on launcher** (e.g., "Earn €1000" not just "Earn")
* [ ] **Label updates within 1-2 seconds on FIRST LOGIN** (not just after page refresh)
* [ ] Browser console shows successful `getLabels` call
* [ ] Launcher degrades gracefully if labels fail to load (shows fallback text)
* [ ] Labels persist correctly during navigation within SPA (don't require page refresh)

**Phase 2 - Enhanced Error Handling & Loading States (Optional but Recommended):**

* [ ] Widget boot errors are logged with detailed information
* [ ] Failed API calls include request/response details in logs
* [ ] Retry logic attempts boot up to 3 times on failure
* [ ] Attribution errors don't block page load or form submission
* [ ] Launcher shows loading state while fetching labels (visual feedback)
* [ ] User sees appropriate feedback for connection issues (if implementing UI indicators)

***

## Step 3: Public Attribution

**📦 Required Script:** `cello-attribution.js` (Attribution Tracking)

This step integrates attribution tracking for **public visitors**. This script captures referral codes (UCC) from URL parameters when users visit via referral links, before they authenticate.

### 3.1 Documentation

* [Embedded Script Tag](/sdk/client-side/embedded-script-tag)
* [Personalizing Referrals](/guides/user-experience/personalizing-referrals)

### 3.2 Prerequisites

* `CELLO_PRODUCT_ID` environment variable set
* Public layout/pages (landing page, signup page)

### 3.3 Integration Description

#### A. Attribution Script URL Helper

Use the same script URLs as in [Embedded Script Tag](/sdk/client-side/embedded-script-tag) (path includes `/latest/`).

```typescript theme={null}
export function getCelloAttributionScriptUrl(): string {
  const env = process.env.CELLO_ENV || "sandbox"
  return env === "production"
    ? "https://assets.cello.so/attribution/latest/cello-attribution.js"
    : "https://assets.sandbox.cello.so/attribution/latest/cello-attribution.js"
}
```

**CRITICAL URLs:**

* ✅ Sandbox: `https://assets.sandbox.cello.so/attribution/latest/cello-attribution.js`
* ✅ Production: `https://assets.cello.so/attribution/latest/cello-attribution.js`
* ❌ Wrong: `https://sandbox.cello.so/attribution/cello-attribution.js`
* ❌ Wrong: `https://assets.sandbox.cello.so/attribution/cello-attribution.js` (missing `/latest/` → Access Denied)

#### B. Add Scripts to Public Layout (cello-attribution.js)

**CRITICAL: Use cello-attribution.js for public pages**

This script captures referral codes from URL parameters. Do not confuse with `cello.js` (used in Step 2 for authenticated users).

Per [Embedded Script Tag](/sdk/client-side/embedded-script-tag): use `type="module"` and `async` on the script tag.

```tsx theme={null}
export default function PublicLayout({ children }) {
  const attributionScriptUrl = getCelloAttributionScriptUrl()
  
  return (
    <html>
      <body>
        {/* Attribution queue snippet - CRITICAL: Use Cello's official queue function */}
        <script
          dangerouslySetInnerHTML={{
            __html: `window.CelloAttribution=window.CelloAttribution||function(t,...o){if("getReferral"===t)throw new Error("getReferral is not supported in this context. Use getUcc instead.");let e,n;const i=new Promise((t,o)=>{e=t,n=o});return window.CelloAttributionCmd=window.CelloAttributionCmd||[],window.CelloAttributionCmd.push({command:t,args:o,resolve:e,reject:n}),i}`
          }}
        />
        
        {/* Attribution script - type="module" and async per docs.cello.so/sdk/client-side/embedded-script-tag */}
        <script
          src={attributionScriptUrl}
          type="module"
          async
          crossOrigin="anonymous"
        />
        
        {children}
      </body>
    </html>
  )
}
```

<Note>
  * Attribution script does NOT require `data-product-id` attribute (product context comes from URL parameters)
  * Use Cello's official queue function (not the simple queue snippet) - this handles async loading and allows immediate calls
  * Queue snippet MUST load before the attribution script
</Note>

#### C. Create Attribution Hook

First, add TypeScript declarations for the Attribution SDK:

```typescript theme={null}
// types/cello-attribution.d.ts (or at top of hook file)
declare global {
  interface Window {
    CelloAttribution?: {
      (command: "getUcc"): Promise<string | null>
      (command: "getReferrerName"): Promise<string | null>
      (command: "getCampaignConfig"): Promise<CampaignConfig | null>
    }
  }
}

interface CampaignConfig {
  newUserDiscountPercentage: number  // Decimal format: 0.1 = 10%, 0.25 = 25%
  newUserDiscountMonth: number       // Duration in months
}
```

Then create the attribution hook:

```typescript theme={null}
"use client"

import { useState, useEffect } from "react"

export function useCelloAttribution() {
  const [ucc, setUcc] = useState<string | null>(null)
  const [referrerName, setReferrerName] = useState<string | null>(null)
  const [campaignConfig, setCampaignConfig] = useState<CampaignConfig | null>(null)
  const [isReady, setIsReady] = useState(false)

  useEffect(() => {
    let cancelled = false

    async function fetchAttribution() {
      try {
        // Direct calls work immediately thanks to queue snippet
        // No polling needed - the queue handles async loading automatically
        const [code, name, config] = await Promise.all([
          window.CelloAttribution("getUcc"),
          window.CelloAttribution("getReferrerName"),
          window.CelloAttribution("getCampaignConfig"),
        ])
        
        if (!cancelled) {
          setUcc(code)
          setReferrerName(name)
          setCampaignConfig(config)
          setIsReady(true)
        }
      } catch (e) {
        console.error("[Cello] Attribution fetch failed:", e)
        if (!cancelled) {
          setIsReady(true)  // Mark ready even without data
        }
      }
    }

    fetchAttribution()
    return () => { cancelled = true }
  }, [])

  return { ucc, referrerName, campaignConfig, isReady }
}

/**
 * Utility to get UCC directly from CelloAttribution
 * Works immediately thanks to queue pattern - no need to check if SDK is loaded
 */
export async function getCelloUcc(): Promise<string | null> {
  try {
    return await window.CelloAttribution("getUcc")
  } catch {
    return null
  }
}
```

**Key Implementation Notes:**

* ✅ **No polling needed** - Cello's queue function handles async loading automatically
* ✅ **Direct calls work immediately** - queue buffers commands until script loads
* ✅ **Simpler and more reliable** - uses Cello's official recommended pattern
* ❌ **Don't use simple queue snippet** - use the full queue function that returns promises

#### D. Signup Personalization (Phase 2 - Optional)

Display referrer name and campaign discount on signup page to improve conversion.

**Documentation:** [getCampaignConfig](/sdk/client-side/attribution-js-usage#getcampaignconfig), [getReferrerName](/sdk/client-side/attribution-js-usage#getreferrername)

```tsx theme={null}
export function SignupReferralBanner() {
  const { referrerName, campaignConfig } = useCelloAttribution()
  
  // Only show banner if user came via referral
  if (!referrerName) return null
  
  // Calculate discount display values
  const hasDiscount = campaignConfig?.newUserDiscountPercentage > 0 && campaignConfig?.newUserDiscountMonth > 0
  const discountPercent = hasDiscount ? Math.round(campaignConfig.newUserDiscountPercentage * 100) : 0
  const discountMonths = campaignConfig?.newUserDiscountMonth || 0
  
  // Format discount message based on duration
  const discountMessage = hasDiscount
    ? discountMonths === 1
      ? `${discountPercent}% off your first month`
      : `${discountPercent}% off your first ${discountMonths} months`
    : null
  
  return (
    <div className="referral-banner">
      <p>{referrerName} invited you to join!</p>
      {discountMessage && <p>🎉 Special offer: {discountMessage}</p>}
    </div>
  )
}
```

**Key Implementation Notes:**

* **Referrer Name Only**: If `campaignConfig` is empty/null or discount values are 0, show only the referrer name as social proof
* **With Discount**: If campaign has a discount configured, display both referrer name and the special offer
* **Singular vs Plural**: Format correctly based on `newUserDiscountMonth`:
  * `newUserDiscountMonth === 1`: "X% off your first month"
  * `newUserDiscountMonth > 1`: "X% off your first Y months"
* **Percentage Conversion**: Convert decimal format (0.1) to display format (10%)

**Example Campaign Config Response:**

```json theme={null}
{
  "newUserDiscountPercentage": 0.3,
  "newUserDiscountMonth": 3
}
```

Displays as: "30% off your first 3 months"

**Empty/No Discount:**

```json theme={null}
{
  "newUserDiscountPercentage": 0,
  "newUserDiscountMonth": 0
}
```

Or `null` - Shows only: "John invited you to join!" (social proof only)

### 3.4 Acceptance Criteria

**Phase 1 - Core Attribution (Mandatory):**

* [ ] Attribution script loads on public pages (check Network tab)
* [ ] `window.CelloAttribution` function is available after script loads
* [ ] Visiting `/?ucc=TEST123` stores the referral code
* [ ] `useCelloAttribution()` hook returns the stored UCC

**Phase 2 - Enhanced Attribution (Optional):**

* [ ] Hook returns error states for debugging
* [ ] Referrer name displays on signup page when visiting via referral link
* [ ] Campaign configuration fetched alongside UCC and referrer name
* [ ] Campaign discount displays correctly on signup page (e.g., "30% off your first 3 months")
* [ ] Signup banner shows only referrer name when no discount is configured (social proof)
* [ ] Discount format adjusts for singular vs plural months ("month" vs "months")
* [ ] Timeout errors are non-blocking and logged clearly
* [ ] Attribution failures don't crash the page

***

## Step 4: Signup Event Tracking

### 4.1 Documentation

* [Track Signups - POST /events](/attribution/tracking-signups#option-3:-using-cello-api-post-/events-api-endpoint)
* [API Authentication](/api-reference/authentication/get-token)
* [Send Event API](/api-reference/generic-events/send-event)

### 4.2 Prerequisites

* `CELLO_ACCESS_KEY_ID` and `CELLO_SECRET_ACCESS_KEY` environment variables set
* Attribution from Step 3 working
* Signup API endpoint

### 4.3 Integration Description

#### A. API Authentication Helper

```typescript theme={null}
const CELLO_API_BASE_URL = process.env.CELLO_API_BASE_URL 
  || (process.env.CELLO_ENV === "production" 
    ? "https://api.cello.so" 
    : "https://api.sandbox.cello.so")

let cachedToken: { token: string; expiresAt: number } | null = null

export async function fetchCelloAccessToken(): Promise<string | null> {
  const accessKeyId = process.env.CELLO_ACCESS_KEY_ID
  const secretAccessKey = process.env.CELLO_SECRET_ACCESS_KEY
  
  if (!accessKeyId || !secretAccessKey) return null
  
  // Return cached token if still valid (with 60s buffer)
  if (cachedToken && cachedToken.expiresAt > Date.now() + 60000) {
    return cachedToken.token
  }
  
  const response = await fetch(`${CELLO_API_BASE_URL}/token`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      accessKeyId,
      secretAccessKey,
    }),
  })
  
  if (!response.ok) return null
  
  const data = await response.json()
  const expiresIn = data.expiresIn ?? 3600  // Default 1 hour
  
  cachedToken = {
    token: data.accessToken,  // CRITICAL: Field is "accessToken", not "token"
    expiresAt: Date.now() + expiresIn * 1000,
  }
  
  return cachedToken.token
}
```

**CRITICAL API Base URLs:**

* ✅ Production: `https://api.cello.so`
* ✅ Sandbox: `https://api.sandbox.cello.so`
* ❌ Wrong: `https://sandbox.api.cello.so` (subdomain order reversed!)

**CRITICAL Endpoints (no /v1/ prefix):**

* ✅ Token: `POST /token`
* ✅ Events: `POST /events`
* ❌ Wrong: `/v1/token`, `/v1/events`, `/v1/authentication/token`

**CRITICAL Token Response:**

* The access token is in `data.accessToken`, NOT `data.token`
* Expiration is in `data.expiresIn` (seconds)

#### B. Event Emission Helper

```typescript theme={null}
export async function emitReferralUpdatedEvent(
  user: { id: string; email: string; name: string },
  ucc: string
): Promise<boolean> {
  const token = await fetchCelloAccessToken()
  if (!token) return false
  
  const response = await fetch(`${CELLO_API_BASE_URL}/events`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      eventName: "ReferralUpdated",
      payload: {
        ucc,
        newUserId: user.id,  // CRITICAL: newUserId in payload
      },
      context: {
        newUser: {
          id: user.id,
          email: user.email,
          name: user.name,  // Use "name", not "fullName"
        },
        event: {
          trigger: "new-signup",
          timestamp: new Date().toISOString(),
        },
      },
    }),
  })
  
  return response.ok
}
```

**CRITICAL Event Payload Structure:**

```json theme={null}
{
  "eventName": "ReferralUpdated",
  "payload": {
    "ucc": "referral-code",
    "newUserId": "user-123",    // ✅ newUserId in PAYLOAD
    "price": 0,                 // ✅ Always include, use 0 for free signups
    "currency": ""              // ✅ Always include, use "" for free signups
  },
  "context": {
    "newUser": {                // ✅ Nested object
      "id": "user-123",
      "email": "user@email.com",
      "name": "John Doe"        // ✅ "name" not "fullName"
    },
    "event": {
      "trigger": "new-signup",
      "timestamp": "2024-01-01T00:00:00Z"
    }
  }
}
```

**Common Mistakes:**

* ❌ Putting `newUserId` in `context` instead of `payload`
* ❌ Using flat fields like `newUserEmail` instead of `newUser.email`
* ❌ Using `fullName` instead of `name`

#### C. Referral Code Validation Helper (Phase 2 - Optional)

**Why Validate?**

* Prevents storing invalid/expired referral codes in your database
* Avoids sending unnecessary events to Cello for invalid codes
* Provides better user feedback for invalid referral links
* Can retrieve campaign metadata for personalization

**Documentation:** [Fetch Referral Code Info](/api-reference/referral-codes/fetch-referral-code-info)

##### Simple Implementation (Returns Boolean)

```typescript theme={null}
export async function validateReferralCode(code: string): Promise<boolean> {
  const token = await fetchCelloAccessToken()
  if (!token) return false
  
  const response = await fetch(`${CELLO_API_BASE_URL}/referral-codes/${code}`, {
    method: "GET",
    headers: {
      "Authorization": `Bearer ${token}`,
    },
  })
  
  if (!response.ok) return false
  
  const data = await response.json()
  return data.valid === true
}
```

##### Enhanced Implementation (Returns Full Response)

```typescript theme={null}
interface ReferralCodeValidation {
  code: string
  valid: boolean
  productUserId?: string
  campaignId?: string
}

export async function validateReferralCode(
  code: string
): Promise<ReferralCodeValidation | null> {
  const token = await fetchCelloAccessToken()
  if (!token) return null
  
  const response = await fetch(`${CELLO_API_BASE_URL}/referral-codes/${code}`, {
    method: "GET",
    headers: {
      "Authorization": `Bearer ${token}`,
    },
  })
  
  if (!response.ok) {
    // 404 = code doesn't exist
    return { code, valid: false }
  }
  
  const data = await response.json()
  return data
}
```

**API Response Structure:**

```json theme={null}
{
  "code": "UIPIWa2Hnnr",
  "productUserId": "12345678",
  "valid": true,
  "campaignId": "campaign_partners_1"
}
```

**Response Fields:**

* `code` (string) - The referral code that was validated
* `valid` (boolean) - Whether the code is currently valid
* `productUserId` (string) - The user ID who owns this referral code
* `campaignId` (string) - The campaign this referral code belongs to

**Common Mistakes:**

* ❌ Not checking `response.ok` before parsing JSON
* ❌ Assuming 404 means error instead of invalid code
* ❌ Not handling missing `productUserId` or `campaignId` (they're optional in response)
* ❌ Blocking signup flow when validation fails (validation should be non-blocking)

#### D. Extend User Model

Add referral fields to your user model:

```typescript theme={null}
interface User {
  id: string
  email: string
  name: string
  // Add these fields
  referralCode?: string
  referralCampaignId?: string
}
```

#### E. Update Signup Handler

##### Phase 1 Implementation (No Validation)

```typescript theme={null}
// In your signup API handler
export async function handleSignup(data: SignupData) {
  const { name, email, password, referralCode } = data
  
  // 1. Create the user
  const user = await createUser({ name, email, password })
  
  // 2. If referral code provided, store and emit event
  if (referralCode) {
    // Store on user
    user.referralCode = referralCode
    await saveUser(user)
    
    // Emit event to Cello (fire and forget)
    emitReferralUpdatedEvent(user, referralCode)
      .then(success => {
        if (success) console.log("[Cello] Referral event sent")
        else console.error("[Cello] Referral event failed")
      })
      .catch(console.error)
  }
  
  return user
}
```

##### Phase 2 Implementation (With Validation)

```typescript theme={null}
// In your signup API handler
export async function handleSignup(data: SignupData) {
  const { name, email, password, referralCode } = data
  
  // 1. Create the user
  const user = await createUser({ name, email, password })
  
  // 2. If referral code provided, validate before storing
  if (referralCode) {
    const validation = await validateReferralCode(referralCode)
    
    if (validation && validation.valid) {
      // Store referral code and campaign ID
      user.referralCode = referralCode
      user.referralCampaignId = validation.campaignId
      await saveUser(user)
      
      // Emit event to Cello (fire and forget)
      emitReferralUpdatedEvent(user, referralCode)
        .then(success => {
          if (success) console.log("[Cello] Referral event sent")
          else console.error("[Cello] Referral event failed")
        })
        .catch(console.error)
    } else {
      // Invalid code - don't store or emit event
      console.warn("[Cello] Invalid referral code, skipping:", referralCode)
      // Signup continues normally - don't block user registration
    }
  }
  
  return user
}
```

**Benefits of Validation (Phase 2):**

* ✅ Prevents storing expired or invalid referral codes
* ✅ Cleaner database without invalid data
* ✅ Avoids unnecessary API calls to Cello for invalid codes
* ✅ Can store campaign ID for analytics and personalization
* ✅ Better user experience with appropriate feedback

**Important:** Validation should be non-blocking. Even if the referral code is invalid, allow the user to complete signup successfully.

#### F. Pass Referral Code from Signup Form

```tsx theme={null}
// In your signup form component
import { useCelloAttribution, getCelloUcc } from "@/hooks/use-cello-attribution"

const { ucc } = useCelloAttribution()

async function handleSubmit(formData) {
  // Try hook value first, fall back to direct SDK call
  let referralCode = ucc
  
  if (!referralCode) {
    referralCode = await getCelloUcc()
  }
  
  await fetch("/api/signup", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      ...formData,
      referralCode,  // Include referral code
    }),
  })
}
```

### 4.4 Acceptance Criteria

**Phase 1 - Core Event Tracking (Mandatory):**

* [ ] `fetchCelloAccessToken()` successfully retrieves and caches access token
* [ ] Signup with referral code triggers `emitReferralUpdatedEvent()`
* [ ] Server logs show `POST /events` returning status 200
* [ ] Cello dashboard shows the new signup event
* [ ] User model stores `referralCode` field
* [ ] Signup without referral code works normally (no errors)

**Phase 2 - Referral Code Validation (Optional but Recommended):**

* [ ] `validateReferralCode()` successfully validates valid referral codes (returns `true` or valid object)
* [ ] `validateReferralCode()` correctly identifies invalid codes (returns `false` or invalid object)
* [ ] Invalid referral codes are NOT stored in user model
* [ ] Invalid referral codes do NOT trigger `emitReferralUpdatedEvent()`
* [ ] Signup completes successfully even with invalid referral codes (non-blocking)
* [ ] Campaign ID is stored alongside referral code when available (enhanced implementation)

**Phase 2 - Enhanced Logging (Optional but Recommended):**

* [ ] Token request logs include full URL and response details
* [ ] Event emission logs include payload structure
* [ ] Environment variables validated on startup
* [ ] Failed API calls show detailed error context

***

## Step 5: Stripe Integration

### 5.1 Documentation

* [Stripe Webhook Integration](/integrations/webhooks/stripe-webhook)

### 5.2 Prerequisites

* Stripe integration in your application
* Cello API credentials (for storing referral data on Stripe customer)

### 5.3 Integration Description

#### A. Add Metadata to Stripe Customer

When creating or updating Stripe customers, include referral metadata:

```typescript theme={null}
import Stripe from "stripe"

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function createStripeCustomer(user: User) {
  return stripe.customers.create({
    email: user.email,
    name: user.name,
    metadata: {
      userId: user.id,
      // Cello-specific metadata for commission tracking
      cello_ucc: user.referralCode || "",
      new_user_id: user.id,
    },
  })
}
```

**Required Metadata Keys:**

* `cello_ucc` - The referral code (UCC)
* `new_user_id` - Your internal user ID

#### B. Configure Stripe Webhook in Cello Portal

1. Go to Cello Portal → Integrations → Stripe
2. Connect your Stripe account
3. Cello will automatically create webhooks for:
   * `checkout.session.completed`
   * `customer.subscription.created`
   * `customer.subscription.updated`
   * `invoice.paid`
   * `charge.refunded`

### 5.4 Acceptance Criteria

* [ ] Stripe customers have `cello_ucc` and `new_user_id` in metadata
* [ ] Stripe webhook configured in Cello portal
* [ ] Test purchase shows in Cello dashboard with correct attribution

***

## Troubleshooting

### Widget Script Fails to Load (ERR\_CONNECTION\_REFUSED)

**Symptoms:**

* Browser console shows `GET https://sandbox.cello.so/widget/cello.js net::ERR_CONNECTION_REFUSED`
* Widget never boots

**Root Cause:**
Script URL format is incorrect (wrong host or path).

**Solution:**

* ✅ Sandbox: `https://assets.sandbox.cello.so/app/latest/cello.js`
* ✅ Production: `https://assets.cello.so/app/latest/cello.js`
* ❌ Wrong: `https://sandbox.cello.so/widget/cello.js`

***

### Widget Script Loads but Doesn't Boot

**Symptoms:**

* Script loads successfully (200 response)
* No boot success message
* Widget never appears

**Common Causes:**

1. **Missing script attributes** - Must have `type="module"`, `async`, `crossOrigin="anonymous"`
2. **Wrong queue pattern** - Use `window.cello = { cmd: [] }` (object with array)
3. **Wrong boot pattern** - Use `window.cello.cmd.push(async function(cello) { await cello.boot(config) })`

***

### User is Not Authorized to Load the Widget

**Symptoms:**

* Script loads successfully
* Console error: "User is not authorized to load the widget"

**Root Cause:**
JWT payload structure is incorrect.

**Solution - Use Cello's custom claim names:**

❌ **WRONG (Standard JWT claims):**

```json theme={null}
{
  "iss": "product-id",
  "sub": "user-123",
  "name": "John Doe",
  "email": "user@example.com",
  "exp": 1662716365
}
```

✅ **CORRECT (Cello's custom claims):**

```json theme={null}
{
  "productId": "product-id",
  "productUserId": "user-123",
  "iat": 1662712365
}
```

**Key Points:**

* Use `productId`, NOT `iss`
* Use `productUserId`, NOT `sub`
* Do NOT include `name`, `email`, `exp` in JWT
* User details go in `productUserDetails` during boot, not JWT
* Algorithm must be `HS512`

***

### Dynamic Labels Don't Appear on First Login (Only After Refresh)

**Symptoms:**

* Custom launcher shows fallback text (e.g., "Invite & Earn") on initial login
* Labels appear correctly after page refresh
* Browser console shows successful `getLabels` call but UI doesn't update

**Root Cause:**
This is a state management/timing issue. The launcher component renders before labels are fetched, and without proper state communication, the component doesn't re-render when labels become available.

**Why It Works After Refresh:**
On page refresh, the component re-initializes and can immediately access labels that were previously stored (e.g., in `window.__celloLabels` or state).

**Solution:**

Implement a communication mechanism between the bootstrap component (which fetches labels) and the launcher component (which displays them):

1. **Event-Based Pattern** (Recommended - framework agnostic):
   * Bootstrap: Fetch labels after boot, store in `window.__celloLabels`, dispatch CustomEvent
   * Launcher: Listen for event, update state when received
   * Check for existing labels on mount (handles navigation)

2. **React Context Pattern**:
   * Create CelloLabelsContext that listens for labels event
   * Wrap authenticated app with provider
   * Launcher consumes context

3. **State Management**:
   * Dispatch labels to Redux/Zustand/etc. after boot
   * Launcher subscribes to store

**Code Example (Event-Based):**

```typescript theme={null}
// Bootstrap: Broadcast labels
window.cello.cmd.push(async function(cello) {
  await cello.boot(config)
  const labels = await window.Cello("getLabels")
  
  window.__celloLabels = labels  // Store globally
  window.dispatchEvent(new CustomEvent("cello:labels:ready", { detail: labels }))
})

// Launcher: Listen for labels
useEffect(() => {
  // Check if already available
  if (window.__celloLabels?.customLauncher) {
    setLabelText(window.__celloLabels.customLauncher)
  }
  
  // Listen for event
  const handler = (e: CustomEvent) => setLabelText(e.detail?.customLauncher)
  window.addEventListener("cello:labels:ready", handler)
  return () => window.removeEventListener("cello:labels:ready", handler)
}, [])
```

**Reference:** See detailed implementation patterns in [Step 2.3.D - Dynamic Reward Labels](#d-custom-launcher)

***

### Attribution Script Fails to Load

**Symptoms:**

* `window.CelloAttribution` never becomes available
* Network tab shows 404 for attribution script

**Root Cause:**
Attribution script URL is incorrect.

**Solution:**

* ✅ Sandbox: `https://assets.sandbox.cello.so/attribution/latest/cello-attribution.js`
* ✅ Production: `https://assets.cello.so/attribution/latest/cello-attribution.js`
* ❌ Wrong: `https://sandbox.cello.so/attribution/cello-attribution.js`

**Also check:**

* Script must have `data-product-id` attribute
* Script must have `type="module"` and `async` attributes

***

### Cello API Returns 404

**Symptoms:**

* Server logs show 404 for `/token` or `/events`
* No referral validation or event emission

**Root Cause:**
API base URL or endpoint paths are incorrect.

**Solution:**

**Correct Base URLs:**

* ✅ Production: `https://api.cello.so`
* ✅ Sandbox: `https://api.sandbox.cello.so`
* ❌ Wrong: `https://sandbox.api.cello.so` (subdomain order reversed!)

**Correct Endpoints (no /v1/ prefix):**

* ✅ `POST /token`
* ✅ `POST /events`
* ✅ `GET /referral-codes/{code}`
* ❌ Wrong: `/v1/token`, `/v1/events`

***

### Access Token is Undefined

**Symptoms:**

* Token request succeeds (200 status)
* But `cachedToken.token` is `undefined`
* All subsequent API calls fail with 401

**Root Cause:**
Reading the wrong field from the token response.

**Solution:**

❌ **WRONG:**

```typescript theme={null}
cachedToken = {
  token: data.token,  // Wrong field name!
}
```

✅ **CORRECT:**

```typescript theme={null}
cachedToken = {
  token: data.accessToken,  // Correct field name
}
```

The Cello API returns `accessToken`, not `token`.

***

### Event Emission Returns 400 (Validation Error)

**Symptoms:**

* Server logs show `Response status: 400`
* Error: `{"message":"Validation error: Missing newUserId"}`

**Root Cause:**
Event payload structure doesn't match Cello's API spec.

**Solution:**

❌ **WRONG (newUserId in wrong location):**

```json theme={null}
{
  "payload": { "ucc": "code123" },
  "context": { "newUserId": "user-123" }
}
```

❌ **WRONG (flat fields instead of nested object):**

```json theme={null}
{
  "context": {
    "newUserEmail": "user@email.com",
    "newUserFullName": "John Doe"
  }
}
```

✅ **CORRECT (per [official API docs](/api-reference/generic-events/send-event)):**

```json theme={null}
{
  "eventName": "ReferralUpdated",
  "payload": {
    "ucc": "code123",
    "newUserId": "user-123"
  },
  "context": {
    "newUser": {
      "id": "user-123",
      "email": "user@email.com",
      "name": "John Doe"
    },
    "event": {
      "trigger": "new-signup",
      "timestamp": "2024-01-01T00:00:00Z"
    }
  }
}
```

**Key Points:**

* `newUserId` must be in `payload`, NOT `context`
* User details go in `context.newUser` as nested object
* Use `name`, NOT `fullName`

***

### Hydration Mismatch Errors

**Symptoms:**

* React console warning about hydration mismatch
* Error mentions attributes like `data-feedly-mini`

**Root Cause:**
Browser extensions modifying the DOM before React hydrates.

**Solution:**
This is not a Cello integration issue. Options:

1. Ignore the warning (functionality still works)
2. Test in incognito mode (extensions disabled)
3. Disable the offending browser extension

***

### Attribution Methods Return undefined Despite Script Loading

**Symptoms:**

* Attribution script loads successfully (200 response)
* `window.CelloAttribution` is a function
* Console logs show `{ucc: undefined, referrerName: undefined}`
* But manual calls in console work: `await window.CelloAttribution("getUcc")` returns data

**Root Cause:**
Using incorrect queue snippet or calling methods before SDK initializes URL parsing.

**Solution:**

❌ **WRONG (Simple queue snippet):**

```javascript theme={null}
window.CelloAttribution = window.CelloAttribution || function(){
  (window.CelloAttribution.q = window.CelloAttribution.q || []).push(arguments)
};
```

✅ **CORRECT (Cello's official queue function):**

```javascript theme={null}
window.CelloAttribution=window.CelloAttribution||function(t,...o){
  if("getReferral"===t)throw new Error("getReferral is not supported. Use getUcc instead.");
  let e,n;const i=new Promise((t,o)=>{e=t,n=o});
  return window.CelloAttributionCmd=window.CelloAttributionCmd||[],
  window.CelloAttributionCmd.push({command:t,args:o,resolve:e,reject:n}),i
}
```

**Why It Matters:**

* Simple queue doesn't return promises - calls execute but results aren't captured
* Official queue function returns promises and properly resolves them when script loads
* No polling or delays needed with the correct queue function

**Reference:** [Cello Attribution Docs - Queue Function](/sdk/client-side/attribution-js-usage#option-1-queue-function-recommended)

***

## Phase 1 vs Phase 2 Feature Matrix

Use this matrix when creating your integration plan to ensure you include all critical features and identify which enhancements to prioritize.

### Phase 1: Mandatory Core Features

| Feature                        | Step         | Why Mandatory                                      |
| ------------------------------ | ------------ | -------------------------------------------------- |
| **JWT Generator**              | 1.2.A        | Required for widget authentication                 |
| **Script URL Resolvers**       | 1.2.B, 3.3.A | Required to load cello.js and cello-attribution.js |
| **Boot Configuration**         | 1.2.C        | Required to initialize widget                      |
| **Widget Token Endpoint**      | 2.3.A        | Required for authenticated boot                    |
| **Widget Bootstrap Component** | 2.3.C        | Required to boot widget                            |
| **Widget Scripts in Layout**   | 2.3.B        | Required to load widget SDK                        |
| **Custom Launcher**            | 2.3.D        | Optional launcher, but...                          |
| **→ Dynamic Labels**           | 2.3.D        | **REQUIRED if using custom launcher**              |
| **Attribution Scripts**        | 3.3.B        | Required to track referral codes                   |
| **Attribution Hook**           | 3.3.C        | Required to read referral codes                    |
| **API Authentication**         | 4.3.A        | Required for event emission                        |
| **Event Emission**             | 4.3.B        | Required to track signups                          |
| **Signup Form Integration**    | 4.3.E        | Required to pass referral code                     |
| **Database Support**           | 4.3.C        | Required to store referral codes                   |
| **Stripe Metadata**            | 5.3.A        | Required for payment attribution                   |

### Phase 2: Optional Enhancements

| Feature                      | Benefit                                 | Priority |
| ---------------------------- | --------------------------------------- | -------- |
| **Environment Validation**   | Catch config issues early               | High     |
| **Detailed API Logging**     | Debug production issues                 | High     |
| **Retry Logic**              | Handle transient failures               | High     |
| **Referral Code Validation** | Prevent invalid codes, cleaner data     | High     |
| **Error State Tracking**     | Better debugging                        | Medium   |
| **Loading States**           | Better UX                               | Medium   |
| **Enhanced Signup Banner**   | Display referrer name + discount offers | Medium   |
| **Campaign Configuration**   | Fetch discount/campaign data            | Medium   |
| **Status Indicators**        | Development debugging                   | Low      |

### Critical Distinction

**Custom Launcher Labels:**

* Custom launcher itself = Optional (can use Cello's default floating button)
* Dynamic labels for custom launcher = **REQUIRED** if you implement custom launcher

**Error Handling:**

* Basic error logging = Part of Phase 1 code (console.error)
* Enhanced error handling = Phase 2 (retry logic, detailed logs, environment validation)

**Attribution:**

* Basic UCC tracking = Phase 1 (required)
* Campaign configuration with discount display = Phase 2 (optional enhancement)

### Planning Checklist

When creating your integration plan:

**Phase 1 Plan Must Include:**

* [ ] All items marked "Mandatory" in matrix above
* [ ] Dynamic labels IF implementing custom launcher
* [ ] Basic error handling (console.error for failures)
* [ ] All acceptance criteria from each step

**Phase 2 Plan Should Consider:**

* [ ] Environment validation (prevents silent failures)
* [ ] Retry logic (improves reliability)
* [ ] Detailed logging (enables debugging)
* [ ] Loading states (improves UX)
* [ ] Enhanced personalization (improves conversion)

**Questions to Ask Before Starting:**

1. Are we using a custom launcher or Cello's default? (If custom → add dynamic labels to Phase 1)
2. Do we need campaign-specific features? (If yes → add to Phase 2)
3. What's our error monitoring strategy? (Determines Phase 2 priority)
4. When do we plan to go to production? (If soon → prioritize Phase 2 error handling)

***

## Quick Reference

### Script URLs

| Environment | Widget Script                                         | Attribution Script                                                        |
| ----------- | ----------------------------------------------------- | ------------------------------------------------------------------------- |
| Sandbox     | `https://assets.sandbox.cello.so/app/latest/cello.js` | `https://assets.sandbox.cello.so/attribution/latest/cello-attribution.js` |
| Production  | `https://assets.cello.so/app/latest/cello.js`         | `https://assets.cello.so/attribution/latest/cello-attribution.js`         |

### API Base URLs

| Environment | Base URL                       |
| ----------- | ------------------------------ |
| Sandbox     | `https://api.sandbox.cello.so` |
| Production  | `https://api.cello.so`         |

### API Endpoints

| Purpose                | Method | Endpoint                 |
| ---------------------- | ------ | ------------------------ |
| Get access token       | POST   | `/token`                 |
| Validate referral code | GET    | `/referral-codes/{code}` |
| Send event             | POST   | `/events`                |

### Token Response

```json theme={null}
{
  "accessToken": "eyJ...",  // Use this field, NOT "token"
  "expiresIn": 3600
}
```

### JWT Structure

```json theme={null}
{
  "productId": "your-product-id",
  "productUserId": "user-123",
  "iat": 1662712365
}
```

### Event Payload Structure

```json theme={null}
{
  "eventName": "ReferralUpdated",
  "payload": {
    "ucc": "referral-code",
    "newUserId": "user-123",
    "price": 0,
    "currency": ""
  },
  "context": {
    "newUser": {
      "id": "user-123",
      "email": "user@email.com",
      "name": "John Doe"
    },
    "event": {
      "trigger": "new-signup",
      "timestamp": "2024-01-01T00:00:00Z"
    }
  }
}
```
