Boom Labs · Integration Runbook

Replyee chat leads → client portals

Push every lead a Replyee chatbot captures straight into the client's own admin portal (e.g. Big Mike's Bail Bonds → portal.bigmikesbailbonds.com/admin/leads), the moment it happens — alongside the existing DB record and owner email alert.

Replyee side — CODE WRITTEN Migration — must run on db.boommedia.us Portal side — paste + deploy v1 · 2026-07-24
0How it works (the flow) 1Replyee — DB migration (add webhook columns) 2Replyee — the push code (already written) 3Portal — the ingest endpoint (paste into bmbb-portal) 4Portal — leads table & the "CHAT" badge 5Wire it up — set the webhook on the bot 6Test it (curl) 7Deploy checklist

0. How it works

One extra hop. When a visitor drops their email in the Replyee chat widget, Replyee does what it already did — and POSTs the lead to a URL you configure per bot. Your portal receives it and inserts a row. No polling, no shared tables, no cross-app coupling.

1
Visitor uses the chat widgetOn the client's site, leaves email + question in the Replyee lead form.
2
Replyee POST /api/leadsInserts into replyee_leads, bumps the counter, emails the owner (all unchanged).
3
Replyee pushes the webhook NEWIf the bot has a lead_webhook_url, fire-and-forget POST with an X-Replyee-Signature header. Never blocks the visitor; never fails lead capture if the portal is down.
4
Portal POST /api/leads/ingest NEWVerifies the secret, inserts a lead row with source = 'chat'.
5
Appears on /admin/leadsShows up with a CHAT badge next to the existing CONTACT FORM leads. Done.
Why push (webhook), not pull (polling)? The owner email already fires instantly on capture — the portal should feel just as live. Pushing keeps each portal decoupled (it only needs one URL + secret), avoids exposing Replyee's tables cross-app, and needs no cron. If a portal is ever offline, the lead is still safe in replyee_leads and the email still sends.

1. Replyee — DB migration

Adds two nullable columns to replyee_chatbots. Run once on the shared Supabase (db.boommedia.us). Safe to re-run.

Replyee/supabase/schema-lead-webhook.sql

alter table replyee_chatbots
  add column if not exists lead_webhook_url    text,
  add column if not exists lead_webhook_secret text;
Fault-tolerant by design. The push code reads these columns in a separate, error-swallowing query. If you deploy the code before running this migration, lead capture still works perfectly — the webhook step is simply skipped until the columns exist. (Same pattern the widget's bot-config route uses for later-migration columns.)

2. Replyee — the push code

Already written into the repo. This is what was added to the leads route, right after the DB insert + counter bump.

Replyee/src/app/api/leads/route.ts

await supabase.rpc('replyee_increment_lead_count', { bot_id: botId })

// Push the lead to the client's own portal, if configured. Read the webhook
// columns in a SEPARATE, error-swallowing query so an un-migrated DB can't
// break lead capture above.
try {
  const { data: hook } = await supabase
    .from('replyee_chatbots')
    .select('lead_webhook_url, lead_webhook_secret')
    .eq('id', botId)
    .maybeSingle()
  const h = hook as { lead_webhook_url?: string | null; lead_webhook_secret?: string | null } | null
  if (h?.lead_webhook_url) {
    // Fire-and-forget: never make the visitor wait on the client's portal.
    fetch(h.lead_webhook_url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...(h.lead_webhook_secret ? { 'X-Replyee-Signature': h.lead_webhook_secret } : {}),
      },
      body: JSON.stringify({
        source: 'chat',
        bot_id: botId,
        bot_name: bot.name,
        email,
        question: question ?? null,
        session_id: sessionId ?? null,
        captured_at: new Date().toISOString(),
      }),
    }).catch((e) => console.error('[/api/leads] webhook push failed', e))
  }
} catch (e) {
  console.error('[/api/leads] webhook lookup failed', e)
}

The payload your portal will receive

POST {lead_webhook_url}
Content-Type: application/json
X-Replyee-Signature: {lead_webhook_secret}

{
  "source": "chat",
  "bot_id": "b1a2...",
  "bot_name": "Big Mike's Assistant",
  "email": "visitor@example.com",
  "question": "Need to bail out my brother from PBC jail tonight — is anyone available?",
  "session_id": "sess_...",
  "captured_at": "2026-07-24T18:54:00.000Z"
}

3. Portal — the ingest endpoint

This lives in the bmbb-portal repo (the live portal.bigmikesbailbonds.com), which is separate from the SaaS workspace. Drop in this route. It's written for a Next.js App-Router portal on Supabase — adjust table/column names to match the portal's actual leads table (see §4).

bmbb-portal/src/app/api/leads/ingest/route.ts  (new file)

import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

const admin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function POST(req: NextRequest) {
  // 1. Verify the shared secret from Replyee.
  const sig = req.headers.get('x-replyee-signature') || ''
  if (!process.env.REPLYEE_LEAD_SECRET || sig !== process.env.REPLYEE_LEAD_SECRET) {
    return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
  }

  // 2. Read the lead.
  const body = await req.json().catch(() => ({}))
  const { email, question, session_id, bot_name, captured_at } = body || {}
  if (!email) {
    return NextResponse.json({ error: 'email required' }, { status: 400 })
  }

  // 3. Insert into the portal's leads table. Adapt column names to your schema.
  const { error } = await admin.from('leads').insert({
    name: 'Chat visitor',              // the widget only collects email; name is unknown
    email,
    phone: null,
    message: question ?? null,
    source: 'chat',                    // drives the CHAT badge on /admin/leads
    status: 'new',
    external_ref: session_id ?? null,  // ties back to the Replyee conversation
    created_at: captured_at ?? new Date().toISOString(),
  })

  if (error) {
    console.error('[leads/ingest]', error)
    return NextResponse.json({ error: error.message }, { status: 500 })
  }
  return NextResponse.json({ ok: true })
}
Match the real schema. I don't have the bmbb-portal repo in this workspace, so the insert({...}) above is a template. Open the portal's existing leads query (the code behind /admin/leads that renders the "CONTACT FORM" rows) and copy its exact table + column names into this insert. If the contact-form path already has a createLead() helper, call that instead of a raw insert so both sources stay consistent.

4. Portal — leads table & the "CHAT" badge

The Leads page already renders a source badge ("CONTACT FORM") and its subtitle already reads "Inbound inquiries from the website contact form and chat." So the UI is ready — you just need chat rows to carry source = 'chat'.

a) Ensure the table has a source column

-- If it doesn't already exist in bmbb-portal's DB:
alter table leads add column if not exists source text default 'contact_form';
alter table leads add column if not exists external_ref text;

b) Map the source value → badge label

Wherever the page renders the grey badge, add the chat case:

const SOURCE_LABEL = {
  contact_form: 'CONTACT FORM',
  chat: 'CHAT',
}
// ...
<span className="badge">{SOURCE_LABEL[lead.source] ?? 'LEAD'}</span>
CONTACT FORM CHAT ← new

5. Wire it up — set the webhook on the bot

Point Big Mike's Replyee bot at the portal, with a shared secret. Two ways:

Option A — SQL (fastest, do this now)

Generate a secret, then set both columns on the bot. Find the bot id first if you don't have it.

-- find the bot
select id, name from replyee_chatbots where name ilike '%big mike%';

-- set the webhook (use a long random secret — e.g. `openssl rand -hex 24`)
update replyee_chatbots
set lead_webhook_url    = 'https://portal.bigmikesbailbonds.com/api/leads/ingest',
    lead_webhook_secret = 'PASTE_A_LONG_RANDOM_SECRET_HERE'
where id = 'THE_BOT_ID';

Then set the same secret on the portal as an env var and redeploy:

REPLYEE_LEAD_SECRET=PASTE_THE_SAME_SECRET_HERE

Option B — dashboard field (nicer, optional later)

Add "Lead webhook URL" + "Secret" inputs to the Replyee bot settings page so any client can self-serve. The API-key settings page is a good place to add them. Not required for BMBB to work today.

Reusable for every client. Each client portal that wants chat leads just needs (1) its own /api/leads/ingest route and (2) a unique secret set on its bot + as its env var. The Replyee side never changes again.

6. Test it

You can test each half independently.

a) Hit the portal ingest directly (proves the portal half)

curl -sS -X POST https://portal.bigmikesbailbonds.com/api/leads/ingest \
  -H "Content-Type: application/json" \
  -H "X-Replyee-Signature: THE_SECRET" \
  -d '{"source":"chat","email":"lead-test@example.com","question":"Test from curl","captured_at":"2026-07-24T19:00:00Z"}'
# → {"ok":true}   then refresh /admin/leads — a CHAT row appears

Wrong/blank secret should return 401 unauthorized.

b) Hit Replyee's leads endpoint (proves the full chain)

curl -sS -X POST https://replyee.online/api/leads \
  -H "Content-Type: application/json" \
  -d '{"botId":"THE_BOT_ID","email":"chain-test@example.com","question":"End-to-end test"}'
# → {"success":true}   the owner gets the email AND the portal gets the CHAT row

7. Deploy checklist

#StepWhere
1Run schema-lead-webhook.sqldb.boommedia.us (shared Supabase)
2Commit + push the Replyee /api/leads change, redeployReplyee (Vercel)
3Add /api/leads/ingest route, matched to the real leads schemabmbb-portal
4Add source/external_ref columns + the CHAT badge casebmbb-portal + its DB
5Set REPLYEE_LEAD_SECRET env var, redeploybmbb-portal (Coolify/Vercel)
6update replyee_chatbots with the URL + same secretdb.boommedia.us
7Run both curl tests
Two open items I can't do from here: (1) the bmbb-portal repo isn't in this GDrive workspace, so steps 3–4 are paste-and-adapt by whoever has that repo; (2) this GDrive Replyee copy has node_modules stripped, so the code change was written but not built — it compiles by construction (same pattern as bot-config), but do a real build on deploy (step 2).
Boom Labs · Replyee → Portal lead sync · Reuses the same push pattern you can point at any client portal.
Files touched in this repo: Replyee/supabase/schema-lead-webhook.sql · Replyee/src/app/api/leads/route.ts