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

# Cello + Lovable Detailed Integration Guide

> Step-by-step Cello integration for Lovable (default launcher, attribution, signup events, Stripe)

# Cello + Lovable Detailed Integration Guide

A step-by-step guide to integrating Cello's core referral system with the default launcher UI. When complete, you'll have a fully functional referral system with Cello's built-in floating action button.

**Estimated Time:** 1 hour

***

## 📋 What This Guide Accomplishes

By the end of this guide, you will have:

✅ **Working Referral Widget** - Users can access their referral link via Cello's default floating button\
✅ **Attribution Tracking** - Referral codes captured from URL parameters\
✅ **Signup Event Tracking** - New signups attributed to referrers\
✅ **Stripe Integration** - Purchases attributed for commission tracking\
✅ **End-to-End Flow** - Complete referral journey from link click to conversion

**Optional enhancements (not covered here):** Custom launcher UI, signup personalization banner, referral code validation, and enhanced error handling. See the [Referral Component](/referral-component/introduction) and [Custom Launcher](/referral-component/custom-launcher) docs for customization.

***

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

***

## 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, and be saved in the database. **Note:** Ensure your server-side functions have appropriate database access to fetch user referral codes. In serverless environments, you may need admin/service credentials rather than user-scoped credentials. |
| **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
```

### 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
4. Configure your reward structure and campaigns

***

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

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

#### 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 with Default Launcher

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

This step integrates the Cello referral widget for **authenticated users only**. This guide uses Cello's **default floating action button** (FAB) - no custom launcher implementation needed. For a custom launcher, see [Custom Launcher](/referral-component/custom-launcher).

### 2.1 Documentation

* [Referral Component Quickstart](/referral-component/quickstart)

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

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

```tsx theme={null}
// Authenticated layout component
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 */}
        <script
          src={celloScriptUrl}
          type="module"
          async
          crossOrigin="anonymous"
        />
        
        <CelloBootstrap />
        {children}
      </body>
    </html>
  )
}
```

**CRITICAL Script Attributes:**

* `type="module"` - Required
* `async` - Required
* `crossOrigin="anonymous"` - Required
* Do NOT include `data-product-id` on the widget script

#### C. Create Bootstrap Component

First, add TypeScript declarations for the Cello SDK. Put them in a **declaration file** (e.g. `types/cello.d.ts`) or at the top of your component file.

<Warning>
  **Do not import .d.ts files.** TypeScript declaration files (`.d.ts`) are included automatically by the compiler. If you add `import ... from '@/types/cello'` or similar, Vite (and many bundlers) will fail because they resolve modules by file and cannot load `.d.ts` as a module. Use a `.d.ts` file and let TypeScript pick it up via your `tsconfig` (no import), or paste the declarations directly in your component.
</Warning>

```typescript theme={null}
// types/cello.d.ts - do NOT import this file; TypeScript includes it automatically
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
}
```

### 2.4 Acceptance Criteria

**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 default floating launcher button
* [ ] Notification badges appear on the launcher (test with your own referral link)

Use Cello's default floating action button.

***

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

### 3.2 Prerequisites

* Public layout/pages (landing page, signup page)

### 3.3 Integration Description

#### A. Attribution Script URL Helper

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

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

```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 */}
        <script
          src={attributionScriptUrl}
          type="module"
          async
          crossOrigin="anonymous"
        />
        
        {children}
      </body>
    </html>
  )
}
```

**CRITICAL:**

* 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

#### C. Create Attribution Hook

First, add TypeScript declarations for the Attribution SDK. Use a declaration file (e.g. `types/cello-attribution.d.ts`) or the top of your hook file. **Do not import the .d.ts file** - TypeScript includes it automatically; importing it will break Vite and other bundlers.

```typescript theme={null}
// types/cello-attribution.d.ts - do NOT import this file; TypeScript includes it automatically
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

The hook fetches `referrerName` and `campaignConfig` for completeness; you can use them for a signup banner (see [Personalizing Referrals](/guides/user-experience/personalizing-referrals)).

### 3.4 Acceptance Criteria

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

Displaying referrer name and campaign discount on the signup page is optional; see [Personalizing Referrals](/guides/user-experience/personalizing-referrals).

***

## 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
  },
  "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. 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
}
```

#### D. Update Signup Handler

```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
}
```

This guide stores referral codes without validation. For validation before storing, see [Fetch Referral Code Info](/api-reference/referral-codes/fetch-referral-code-info).

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

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

Referral code validation before storing is optional; see the API reference above.

***

## 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. UCC Data Flow (Critical)

Understanding how referral codes flow through your system is critical for proper attribution.

**The Complete Flow:**

1. **Capture** (Public Pages): `cello-attribution.js` captures UCC from URL parameter (`?ucc=CODE`)
2. **Save** (Signup): Your signup handler saves UCC to database field `user.referralCode`
3. **Read** (Checkout): Your checkout function reads UCC from database to add to Stripe metadata

**⚠️ Critical: Read from Database at Checkout**

At checkout time, ALWAYS read the referral code from your database, NOT from cookies or frontend.

**Why?**

* Attribution cookies have limited lifetime (typically 30-90 days)
* Users may checkout days or weeks after signup
* Cookies may be cleared by the user
* Database is the source of truth for stored referral codes

**✅ CORRECT Pattern:**

```typescript theme={null}
// In your checkout/subscription creation function
export async function createCheckoutSession(userId: string, priceId: string) {
  // 1. Fetch user data from database (including referral code)
  const user = await getUserById(userId)
  
  // 2. Read referral code from database field
  const referralCode = user.referralCode || ""
  
  // 3. Create or update Stripe customer with Cello metadata
  const customer = await createOrUpdateStripeCustomer({
    ...user,
    referralCode,  // From database, not from cookie
  })
  
  // 4. Create checkout session
  // ...
}
```

**❌ WRONG Pattern:**

```typescript theme={null}
// DON'T: Rely on frontend passing UCC at checkout
export async function createCheckoutSession(req) {
  const { userId, ucc } = await req.json()  // ❌ Cookie may have expired
  
  const customer = await stripe.customers.create({
    metadata: {
      celo_ucc: ucc,  // ❌ May be null even though user signed up with referral
    }
  })
}
```

**Key Takeaway:**

* **Signup**: Save UCC to database (✓ covered in Step 4)
* **Checkout**: Read UCC from database (not from cookies/frontend)
* Database is your persistent source of truth for referral attribution

#### B. Create or Update Stripe Customer with Metadata

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 createOrUpdateStripeCustomer(user: User) {
  // Check if customer already exists
  const existingCustomers = await stripe.customers.list({
    email: user.email,
    limit: 1,
  })
  
  const metadata = {
    userId: user.id,
    // Cello-specific metadata for commission tracking
    cello_ucc: user.referralCode || "",
    new_user_id: user.id,
  }
  
  if (existingCustomers.data.length > 0) {
    // Update existing customer
    return stripe.customers.update(existingCustomers.data[0].id, {
      name: user.name,
      metadata,
    })
  } else {
    // Create new customer
    return stripe.customers.create({
      email: user.email,
      name: user.name,
      metadata,
    })
  }
}
```

**Required Metadata Keys:**

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

**Important:** Always update metadata if the customer already exists. This ensures referral attribution is preserved even if users had a Stripe customer record before signing up with a referral code.

#### C. Stripe Checkout Sessions (Subscription Mode)

If using Stripe Checkout Sessions with `mode: "subscription"`, you **must create the customer first** with metadata, then pass the customer ID to the session.

**⚠️ Important Limitations:**

* `customer_creation: "always"` only works in `payment` mode, NOT `subscription` mode
* `subscription_data.metadata` sets metadata on the **subscription**, not the **customer**
* Cello's webhook reads from **customer metadata**, not subscription metadata

**✅ Correct Pattern for Checkout Sessions:**

```typescript theme={null}
export async function createSubscriptionCheckout(user: User, priceId: string) {
  // 1. Create or retrieve customer FIRST with Cello metadata
  let customer = await findExistingCustomer(user.email)
  
  if (!customer) {
    customer = await stripe.customers.create({
      email: user.email,
      name: user.name,
      metadata: {
        userId: user.id,
        cello_ucc: user.referralCode || "",
        new_user_id: user.id,
      },
    })
  } else {
    // Update existing customer with Cello metadata
    customer = await stripe.customers.update(customer.id, {
      metadata: {
        userId: user.id,
        cello_ucc: user.referralCode || "",
        new_user_id: user.id,
      },
    })
  }

  // 2. Pass customer ID to checkout session
  const session = await stripe.checkout.sessions.create({
    customer: customer.id,  // ✅ Use customer ID, not customer_email
    mode: "subscription",
    line_items: [{
      price: priceId,
      quantity: 1,
    }],
    success_url: `${process.env.APP_URL}/success`,
    cancel_url: `${process.env.APP_URL}/cancel`,
  })
  
  return session
}
```

**❌ Common Mistakes:**

```typescript theme={null}
// DON'T: This doesn't work in subscription mode
const session = await stripe.checkout.sessions.create({
  customer_email: user.email,
  customer_creation: "always",  // Only works in payment mode!
  mode: "subscription",
  // ...
})

// DON'T: This sets metadata on subscription, not customer
const session = await stripe.checkout.sessions.create({
  customer_email: user.email,
  mode: "subscription",
  subscription_data: {
    metadata: {  // ❌ Cello can't read this
      cello_ucc: user.referralCode,
    }
  },
  // ...
})
```

**Key Points:**

* Always create/update the customer before creating the checkout session
* Pass `customer: customer.id` to the session, not `customer_email`
* Cello metadata MUST be on the customer object, not the subscription

#### D. Edge Function Database Access

Issue: When reading user data (like referral\_code) from edge functions, the Supabase client created with SUPABASE\_ANON\_KEY cannot bypass RLS policies. Even after verifying a user's JWT with auth.getUser(token), subsequent queries run with auth.uid() = null, causing RLS to block the read.

Solution: Use the SUPABASE\_SERVICE\_ROLE\_KEY for server-side reads that need to bypass RLS:

```typescript theme={null}
// ❌ WRONG - RLS blocks this (auth.uid() is null)
const supabaseClient = createClient(
  Deno.env.get("SUPABASE_URL"),
  Deno.env.get("SUPABASE_ANON_KEY")
);
const { data: profile } = await supabaseClient
  .from("profiles")
  .select("referral_code")
  .eq("id", user.id)
  .single(); // Returns null!

// ✅ CORRECT - Service role bypasses RLS
const supabaseAdmin = createClient(
  Deno.env.get("SUPABASE_URL"),
  Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")
);
const { data: profile } = await supabaseAdmin
  .from("profiles")
  .select("referral_code")
  .eq("id", user.id)
  .single(); // Works!
```

<Note>
  Only use the service role key for trusted server-side operations, never expose it client-side.
</Note>

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

<Warning>
  **Once the integration is complete, have the user complete these steps:**
</Warning>

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
* [ ] Reading user data like `referral_code` from edge functions is working correctly

***

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

**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`

***

### 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 `type="module"` and `async` attributes
* Queue snippet MUST load before the attribution script

***

### 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`
* ❌ 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:**

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

***

You now have a fully functional referral system with Cello's default UI. For customization (custom launcher, signup banner, referral code validation), see the [Referral Component](/referral-component/introduction) and [Custom Launcher](/referral-component/custom-launcher) docs.
