Skip to main content

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

IMPORTANT: When planning a Cello integration, structure your plan into TWO PHASES:

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.
Common Mistake: Treating Phase 2 features (especially dynamic labels for custom launchers) as truly optional. They’re essential for a good user experience.
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

Environment Variables

Create these environment variables in your .env file:
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 and Embedded Script Tag in the steps below.

Cello Portal Setup

  1. Create an account at 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
Two Separate Credential Pairs: Cello uses two completely different sets of credentials that are easy to confuse:Before implementing any server-side API calls, read the official API reference:
  1. Configure your reward structure and campaigns

Cello Scripts Overview

Cello provides two separate JavaScript SDKs for different purposes: 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

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.
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 (path includes /latest/).
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

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

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:

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: 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:
Script attributes (from 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:
Then create the bootstrap component:

D. Custom Launcher

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.
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
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:
Common Timing Issue and Solution
Problem: Dynamic labels don’t appear on first login, only after page refresh.
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 Declarations:
2. React Context Pattern For React applications, wrap your authenticated app with a context provider:
Then in your launcher:
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

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 (path includes /latest/).
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: use type="module" and async on the script tag.
  • 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:
Then create the attribution hook:
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, getReferrerName
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:
Displays as: “30% off your first 3 months” Empty/No Discount:
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

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

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

CRITICAL Event Payload Structure:
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
Simple Implementation (Returns Boolean)
Enhanced Implementation (Returns Full Response)
API Response Structure:
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:

E. Update Signup Handler

Phase 1 Implementation (No Validation)
Phase 2 Implementation (With Validation)
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

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

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:
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):
CORRECT (Cello’s custom claims):
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):
Reference: See detailed implementation patterns in Step 2.3.D - Dynamic Reward Labels

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:
CORRECT:
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):
WRONG (flat fields instead of nested object):
CORRECT (per official API docs):
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):
CORRECT (Cello’s official queue function):
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

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

Phase 2: Optional Enhancements

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

API Base URLs

API Endpoints

Token Response

JWT Structure

Event Payload Structure