Skip to main content

Cello + Lovable Detailed Integration (Credit-based Rewards)

This guide extends the standard Lovable Cello integration to support in-product credit rewards (e.g. “500 credits per conversion”) where your platform grants credits to users after Cello issues a reward. When complete, you’ll have:
  • Working Referral Widget (default floating launcher)
  • Attribution Tracking (UCC captured on public pages)
  • Signup Tracking (signup events sent to Cello)
  • Stripe Conversion Tracking (purchase/subscription events via Stripe webhook)
  • Credit Reward Fulfillment (Cello → your backend webhook → apply credits in DB)
Estimated Time: 1–2 hours (depends on how you model credits + idempotency)

What’s different vs cash rewards?

The referral capture, attribution, signup tracking, and Stripe conversion tracking steps are the same as the base guide: The key difference is the last mile: when Cello determines a conversion earned a reward, Cello notifies your platform via a credit reward webhook. Your platform must:
  • Authenticate the webhook request (HTTP Basic Auth)
  • Deduplicate events (at-least-once delivery)
  • Apply credits to the correct user (productUserId)

Prerequisites

Application requirements

RequirementDescription
Auth (Supabase)Users can sign up and log in (Lovable + Supabase).
User ModelEach user has a unique ID, email, and name, saved in the database.
Credits StorageYou have either a credits_balance field or a credits ledger (recommended).
Server-side endpointsYou can create API routes / edge functions to receive webhooks.
Stripe subscription flowStripe is set up (required if your campaign rewards are based on payments/subscriptions).

Environment variables

These are the same as the base Lovable detailed integration plus two new values for the credit rewards webhook.
# 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 Cello API Calls (Signup 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

# Credit Rewards Webhook (provided by Cello Support)
CELLO_CREDIT_REWARDS_WEBHOOK_USERNAME=your-basic-auth-username
CELLO_CREDIT_REWARDS_WEBHOOK_PASSWORD=your-basic-auth-password

Steps 1–5: Implement the base Lovable integration

Complete the following guide end-to-end: Why you still need Steps 1–5
  • Step 2/3: Users need the referral widget + UCC attribution.
  • Step 4: Cello needs the signup event (POST /events) to link the new user to a referrer.
  • Step 5: Cello needs conversion/payment signals (Stripe webhook + customer metadata) to determine eligibility for credit rewards.
Once Steps 1–5 are working, continue below.

Step 6: Credit-based rewards webhook (Cello → your backend)

This step is the core of credit-based rewards: Cello notifies your platform when a credit reward is created, and your platform applies credits.

6.1 Documentation

6.2 Webhook basics

  • Method: POST
  • Authentication: HTTP Basic Auth
  • Headers:
    • Authorization: Basic base64({username}:{password})
    • Content-Type: application/json; charset=utf-8
    • X-Cello-Env: prod | sandbox
  • Delivery semantics: at-least-once (you must dedupe)

6.3 Event payload

Cello sends a new-reward event when a credit reward is issued:
{
  "version": "1.0.3",
  "eventType": "new-reward",
  "createdTs": "2024-07-20T12:34:56.789Z",
  "eventId": "evt_1Rl4G0KEzvleW5flVs9sMRrs",
  "data": {
    "rewardId": "rwd_7f3ac1e0",
    "productUserId": "your_product_user_id_9d1b3e2f",
    "rewardType": "credits",
    "reward": {
      "amount": 500
    }
  }
}
Key fields
  • data.productUserId: the user who should receive credits (this should match your internal user ID used as productUserId when booting cello.js)
  • data.reward.amount: number of credits to grant
  • eventId: stable across retries, use it for deduplication

Step 7: Implement secure + idempotent credit fulfillment

  • Use a ledger (append-only credit transactions) rather than mutating a single balance field.
    • You can still maintain a credits_balance cache, but a ledger makes audit + idempotency much easier.
  • Deduplicate by eventId.
    • Store eventId in a table with a unique constraint so duplicates fail safely.
  • Process asynchronously if your stack allows it.
    • The webhook should respond 2xx quickly; apply credits in a background job if needed.

7.2 Minimal data model (example)

Create a table to dedupe + audit:
  • cello_reward_events
    • event_id (unique)
    • reward_id
    • product_user_id
    • amount
    • env (from X-Cello-Env)
    • received_at
    • raw_payload (JSON)
And either:
  • credit_ledger
    • id
    • user_id
    • source (e.g. cello)
    • source_id (store eventId and/or rewardId)
    • amount
    • created_at
or a users.credits_balance field you increment.

7.3 Webhook handler (Node / TypeScript example)

Use this pattern whether you implement it as a Next.js route handler, an Express route, or a Supabase edge function:
function timingSafeEqual(a: string, b: string) {
  if (a.length !== b.length) return false
  let out = 0
  for (let i = 0; i < a.length; i++) out |= a.charCodeAt(i) ^ b.charCodeAt(i)
  return out === 0
}

function parseBasicAuth(header: string | null): { username: string; password: string } | null {
  if (!header) return null
  const [scheme, encoded] = header.split(" ")
  if (scheme !== "Basic" || !encoded) return null
  const decoded = Buffer.from(encoded, "base64").toString("utf8")
  const idx = decoded.indexOf(":")
  if (idx === -1) return null
  return { username: decoded.slice(0, idx), password: decoded.slice(idx + 1) }
}

export async function handleCelloCreditRewardWebhook(req: Request) {
  // 1) Authenticate (Basic Auth)
  const creds = parseBasicAuth(req.headers.get("authorization"))
  const expectedUser = process.env.CELLO_CREDIT_REWARDS_WEBHOOK_USERNAME || ""
  const expectedPass = process.env.CELLO_CREDIT_REWARDS_WEBHOOK_PASSWORD || ""

  const isAuthed =
    !!creds &&
    timingSafeEqual(creds.username, expectedUser) &&
    timingSafeEqual(creds.password, expectedPass)

  if (!isAuthed) {
    return new Response("Unauthorized", { status: 401 })
  }

  // 2) Parse payload
  const body = await req.json()

  // 3) Ignore unrelated events
  if (body?.eventType !== "new-reward" || body?.data?.rewardType !== "credits") {
    return new Response("Ignored", { status: 200 })
  }

  const eventId = String(body.eventId || "")
  const rewardId = String(body.data.rewardId || "")
  const productUserId = String(body.data.productUserId || "")
  const amount = Number(body.data.reward?.amount)
  const env = req.headers.get("x-cello-env") || "unknown"

  if (!eventId || !rewardId || !productUserId || !Number.isFinite(amount)) {
    // 4xx is OK here; Cello will retry. Prefer strict validation to avoid corrupt state.
    return new Response("Bad Request", { status: 400 })
  }

  // 4) Dedupe (eventId is stable across retries)
  // Pseudocode:
  // - Insert eventId into cello_reward_events with unique constraint
  // - If unique violation -> already processed -> return 200
  // - Otherwise apply credits and return 200

  // await db.transaction(async (tx) => {
  //   const inserted = await tx.insertRewardEvent({ eventId, rewardId, productUserId, amount, env, raw: body })
  //   if (!inserted) return
  //   await tx.addCredits({ userId: productUserId, amount, source: "cello", sourceId: eventId })
  // })

  return new Response("OK", { status: 200 })
}
Critical implementation notes
  • Use a unique constraint on eventId for dedupe. This is the safest pattern under retries and concurrency.
  • Always return 2xx for events you’ve already processed (idempotency).
  • Treat productUserId as your lookup key for which user gets credits.

Step 8: Operational setup (Cello Support)

Per current documentation, the credit reward webhook configuration is coordinated through Cello Support. Share these with Cello Support for both environments:
  • Sandbox webhook endpoint URL
  • Production webhook endpoint URL
Cello Support will also provide:
  • Webhook Basic Auth username
  • Webhook Basic Auth password

Acceptance criteria (credit rewards)

  • A test conversion in sandbox results in a new-reward webhook call to your endpoint
  • Requests without valid Basic Auth return 401
  • A valid new-reward with rewardType: "credits" creates exactly one credit grant (no double credits on retry)
  • Replaying the exact same payload (same eventId) returns 2xx and does not change credits
  • Credits are applied to the user matching data.productUserId

Troubleshooting

Webhook retries keep happening

Symptoms
  • You see the same reward event multiple times
Likely causes
  • Your handler is not returning 2xx
  • Your handler times out
  • You return 5xx for validation failures
Fix
  • Return 2xx for processed events (idempotency)
  • Do strict validation but avoid expensive work synchronously

Credits are applied multiple times

Root cause
  • Missing dedupe by eventId
Fix
  • Store eventId in a table with a unique constraint and short-circuit duplicates

Credits go to the wrong user

Root cause
  • Your productUserId mapping is inconsistent
Fix
  • Ensure the user ID you pass to the widget JWT (productUserId) is the same ID you use in your DB
  • Ensure you apply credits using data.productUserId from the webhook payload

You now have a Lovable-ready Cello referral integration with credit-based rewards: Cello tracks attribution and conversions, and your platform fulfills the credits when rewards are issued.