> ## 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 (Credit-based Rewards)

> Step-by-step Cello integration for Lovable with in-product credit rewards (widget, attribution, signup + Stripe conversion tracking, and credit rewards webhook)

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

* [Cello + Lovable Detailed Integration Guide](/resources/cello-lovable-detailed-integration)

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

| Requirement                  | Description                                                                               |
| ---------------------------- | ----------------------------------------------------------------------------------------- |
| **Auth (Supabase)**          | Users can sign up and log in (Lovable + Supabase).                                        |
| **User Model**               | Each user has a unique ID, email, and name, saved in the database.                        |
| **Credits Storage**          | You have either a `credits_balance` field or a credits ledger (recommended).              |
| **Server-side endpoints**    | You can create API routes / edge functions to receive webhooks.                           |
| **Stripe subscription flow** | Stripe 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.

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

* [Cello + Lovable Detailed Integration Guide](/resources/cello-lovable-detailed-integration)

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

* [Credit-based Rewards](/attribution/credit-based-rewards)

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

```json theme={null}
{
  "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

### 7.1 Design decisions (recommended)

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

```ts theme={null}
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.
