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
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
- ✅ 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. 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
- Create an account at cello.so
- Create a product and note the
Product IDandProduct Secret - Generate API access keys for server-side calls at portal.cello.so/integrations/accesskeys
- 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/launchercello-attribution.js- Loaded in public layouts to capture referral codes from URL parameters
Step 1: Server-Side Helpers
1.1 Documentation
- User Authentication - JWT structure and signing
- Referral Component Quickstart - Boot configuration
1.2 Prerequisites
CELLO_PRODUCT_IDandCELLO_PRODUCT_SECRETenvironment variables set- A JWT signing library (e.g.,
jsonwebtokenfor 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.- ❌ Using
iss,sub,exp(standard JWT claims) - ❌ Including
name,emailin 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/).
- ✅ 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 withproductId,productUserId,iatclaims only -
getCelloScriptUrl()returns correct URL based on environment -
buildCelloBootConfig()returns config withproductUserDetailsincludingfirstName,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 withcello-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:
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:D. Custom Launcher
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
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:- User logs in
- Launcher component renders with fallback text
- Bootstrap fetches labels asynchronously
- Labels are fetched but component doesn’t know about them
- User refreshes → component re-renders with labels already in state ✓
- Boot Cello in bootstrap component
- After boot completes, fetch labels via
window.Cello("getLabels") - Store labels in shared state (global variable, context, or state management)
- Notify components that labels are ready (custom event, callback, or state update)
- Components listen for labels and update UI accordingly
Implementation Approaches
1. Event-Based Pattern (Framework Agnostic) Works with any framework or vanilla JavaScript:- ✅ Always provide fallback text for initial render
- ✅ Store labels in shared state (global variable, context, store)
- ✅ Use event/callback/subscription to notify components
- ✅ Check for existing labels on component mount (handles navigation)
- ✅ Clean up event listeners on unmount
- ❌ 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)
- 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
getLabelscall - Launcher degrades gracefully if labels fail to load (shows fallback text)
- Labels persist correctly during navigation within SPA (don’t require page refresh)
- 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_IDenvironment 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/).
- ✅ 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 withcello.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-idattribute (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:- ✅ 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- Referrer Name Only: If
campaignConfigis 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%)
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.CelloAttributionfunction is available after script loads - Visiting
/?ucc=TEST123stores the referral code -
useCelloAttribution()hook returns the stored UCC
- 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_IDandCELLO_SECRET_ACCESS_KEYenvironment variables set- Attribution from Step 3 working
- Signup API endpoint
4.3 Integration Description
A. API Authentication Helper
- ✅ Production:
https://api.cello.so - ✅ Sandbox:
https://api.sandbox.cello.so - ❌ Wrong:
https://sandbox.api.cello.so(subdomain order reversed!)
- ✅ Token:
POST /token - ✅ Events:
POST /events - ❌ Wrong:
/v1/token,/v1/events,/v1/authentication/token
- The access token is in
data.accessToken, NOTdata.token - Expiration is in
data.expiresIn(seconds)
B. Event Emission Helper
- ❌ Putting
newUserIdincontextinstead ofpayload - ❌ Using flat fields like
newUserEmailinstead ofnewUser.email - ❌ Using
fullNameinstead ofname
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
Simple Implementation (Returns Boolean)
Enhanced Implementation (Returns Full Response)
code(string) - The referral code that was validatedvalid(boolean) - Whether the code is currently validproductUserId(string) - The user ID who owns this referral codecampaignId(string) - The campaign this referral code belongs to
- ❌ Not checking
response.okbefore parsing JSON - ❌ Assuming 404 means error instead of invalid code
- ❌ Not handling missing
productUserIdorcampaignId(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)
- ✅ 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
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 /eventsreturning status 200 - Cello dashboard shows the new signup event
- User model stores
referralCodefield - Signup without referral code works normally (no errors)
-
validateReferralCode()successfully validates valid referral codes (returnstrueor valid object) -
validateReferralCode()correctly identifies invalid codes (returnsfalseor 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)
- 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:cello_ucc- The referral code (UCC)new_user_id- Your internal user ID
B. Configure Stripe Webhook in Cello Portal
- Go to Cello Portal → Integrations → Stripe
- Connect your Stripe account
- Cello will automatically create webhooks for:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedinvoice.paidcharge.refunded
5.4 Acceptance Criteria
- Stripe customers have
cello_uccandnew_user_idin 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
- ✅ 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
- Missing script attributes - Must have
type="module",async,crossOrigin="anonymous" - Wrong queue pattern - Use
window.cello = { cmd: [] }(object with array) - 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”
- Use
productId, NOTiss - Use
productUserId, NOTsub - Do NOT include
name,email,expin JWT - User details go in
productUserDetailsduring 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
getLabelscall but UI doesn’t update
window.__celloLabels or state).
Solution:
Implement a communication mechanism between the bootstrap component (which fetches labels) and the launcher component (which displays them):
-
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)
- Bootstrap: Fetch labels after boot, store in
-
React Context Pattern:
- Create CelloLabelsContext that listens for labels event
- Wrap authenticated app with provider
- Launcher consumes context
-
State Management:
- Dispatch labels to Redux/Zustand/etc. after boot
- Launcher subscribes to store
Attribution Script Fails to Load
Symptoms:window.CelloAttributionnever becomes available- Network tab shows 404 for attribution script
- ✅ 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
- Script must have
data-product-idattribute - Script must have
type="module"andasyncattributes
Cello API Returns 404
Symptoms:- Server logs show 404 for
/tokenor/events - No referral validation or event emission
- ✅ Production:
https://api.cello.so - ✅ Sandbox:
https://api.sandbox.cello.so - ❌ Wrong:
https://sandbox.api.cello.so(subdomain order reversed!)
- ✅
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.tokenisundefined - All subsequent API calls fail with 401
accessToken, not token.
Event Emission Returns 400 (Validation Error)
Symptoms:- Server logs show
Response status: 400 - Error:
{"message":"Validation error: Missing newUserId"}
newUserIdmust be inpayload, NOTcontext- User details go in
context.newUseras nested object - Use
name, NOTfullName
Hydration Mismatch Errors
Symptoms:- React console warning about hydration mismatch
- Error mentions attributes like
data-feedly-mini
- Ignore the warning (functionality still works)
- Test in incognito mode (extensions disabled)
- Disable the offending browser extension
Attribution Methods Return undefined Despite Script Loading
Symptoms:- Attribution script loads successfully (200 response)
window.CelloAttributionis a function- Console logs show
{ucc: undefined, referrerName: undefined} - But manual calls in console work:
await window.CelloAttribution("getUcc")returns data
- 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
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
- Basic error logging = Part of Phase 1 code (console.error)
- Enhanced error handling = Phase 2 (retry logic, detailed logs, environment validation)
- 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
- Environment validation (prevents silent failures)
- Retry logic (improves reliability)
- Detailed logging (enables debugging)
- Loading states (improves UX)
- Enhanced personalization (improves conversion)
- Are we using a custom launcher or Cello’s default? (If custom → add dynamic labels to Phase 1)
- Do we need campaign-specific features? (If yes → add to Phase 2)
- What’s our error monitoring strategy? (Determines Phase 2 priority)
- When do we plan to go to production? (If soon → prioritize Phase 2 error handling)