Embed Happierleads screens
Build it with your AI coding assistant
A ready prompt for Claude, Cursor or any AI coding assistant that builds every step of this section, with a checklist so nothing is forgotten.
Paste this into Claude (or any AI coding assistant) inside your app's code base. It builds every step of this section, the one-off migration included, and keeps a checklist file so nothing gets forgotten. The dashboard steps it asks you to confirm.
You are integrating Happierleads "Embedded screens" into this app. Our clients will see their website visitors (Leads) and the tracking-pixel install screen inside our app, in an iframe, under our brand. Every item below is a hard requirement: missing one breaks the integration for real clients or leaks a key.
BEFORE ANYTHING ELSE: create INTEGRATION_CHECKLIST.md at the repo root containing every checkbox below, word for word. Tick a box only when its code is written AND tested, or, for the manual items, when I have confirmed it. Re-read the checklist at the start of every reply, end every reply with the list of boxes still open, and never say "done" while one is unticked. The dashboard items (section A) are mine: ask me to confirm each one, never assume it.
## Facts
- API base: read it from HL_API_BASE (https://rest-admin.happierleads.com, or https://rest-dash.<our domain> once white-label DNS is verified).
- Two keys, server only: HL_MASTER_KEY (master-priv-…, sent as ?privateKey=) and HL_EMBED_KEY (hle_…, sent as Authorization: Bearer). Never in browser code, never in logs, never in git. Add HL_API_BASE, HL_MASTER_KEY and HL_EMBED_KEY to .env.example with empty values.
- One Happierleads website per client company, its id stored as company.happierleadsWebsiteId (add the field and its migration).
## A. Dashboard setup (manual: I do these, you ask me to confirm)
- [ ] A-1. Account settings → White label → Start setup: our two addresses chosen (e.g. reveal.<our domain> and rest-dash.<our domain>), the DNS records the wizard shows added at our DNS provider, Verify passed.
- [ ] A-2. Account settings → White label → Embed in your app: "Allow my app to embed these screens" ticked, the origin of EVERY environment that shows the iframe saved (production, staging, local), tabs / Leads-screen parts / extra screens chosen, Session length set (1 week by default, 1 minute to 1 year).
- [ ] A-3. Server keys → Create key: saved as HL_EMBED_KEY in every environment's secrets.
- [ ] A-4. Account settings → API Keys: master key created and saved as HL_MASTER_KEY.
## B. Server: one Happierleads client module
- [ ] B-1. createWebsite({ name, domain }) → POST /admin/websites { name, domain } → { websiteId } (domain may be ""); setLimits(id, { credits, emailAccounts }) → PUT /admin/websites/set-limits/{id}; startTrial(id, endsAt) → PUT /admin/websites/toggle-limits/{id} { trialEnabled: true, trialExpireDate, enableWebsiteLimits: true }; setPaid(id, renewsAt) → same URL with { trialEnabled: false, activeWebsite: true, enableWebsiteLimits: true, refreshOnDate }; cancel(id) → setLimits(id, { credits: 0, emailAccounts: 0 }) then toggle-limits { activeWebsite: false }; createEmbedSession(body) → POST /v3/embed/sessions.
- [ ] B-2. Every call: 10 s timeout; a non-2xx throws with method, path and status (never the key or the full URL); one retry on 5xx or network errors only.
## C. Scenario 1: a client signs up
- [ ] C-1. Where a client company is created: createWebsite, save websiteId on the company immediately, then setLimits and startTrial (credits and trial length from our plan config).
- [ ] C-2. If Happierleads fails, sign-up still succeeds: flag the company happierleadsPending and retry from a background job. Never create a second website for a company that already has an id.
## D. One-off migration of existing clients (before launch)
- [ ] D-1. scripts/happierleads-migrate with a --dry-run flag that only prints: every company without happierleadsWebsiteId → createWebsite, save the id, setLimits, then setPaid (paying: refreshOnDate = next renewal) or startTrial (trial end date).
- [ ] D-2. Idempotent (skips companies that have an id, saves the id before the next call), throttled (about 3 calls per second), logs ok / failed per company plus a summary; running it again retries the failures.
- [ ] D-3. Run on staging, then production; show me the summary and I confirm the counts.
## E. Billing: leads follow what they pay
- [ ] E-1. In our billing webhook: payment succeeded (first payment, or trial → paid) → setPaid(next billing date) FIRST, then setLimits({ credits: the plan's leads, emailAccounts, resetCredits: true }). Plan change → setLimits with the new numbers (an upgrade adds the difference to what is left). Cancelled or unpaid → setLimits({ credits: 0, emailAccounts: 0 }) then toggle-limits { activeWebsite: false } (tracking stops, their data is kept).
- [ ] E-1b. Know what Happierleads does on its own, and do NOT duplicate it: a normal monthly renewal needs no call (leads refill to the limit on refreshOnDate, then the date moves on a month); a trial that ends sets the leads to 0 by itself. set-limits always needs BOTH credits and emailAccounts (a 405 otherwise).
- [ ] E-2. The webhook is idempotent (the same event twice gives the same result) and logs the status of each Happierleads call.
## F. Scenario 2: a client logs in and opens the page (the embed endpoint)
- [ ] F-1. GET /api/leads-embed behind our normal auth. Not signed in → 401 without calling Happierleads.
- [ ] F-2. Normal users: createEmbedSession({ websiteId, view }). Send NO user email or name: Happierleads does not need our users' personal data (only add user: { email } if we switch on CSV export, which emails the file link there). websiteId comes from the signed-in user's company, NEVER from the request. view is validated against auto, leads, install and the extra screens we ticked in A-2.
- [ ] F-3. Only our own staff (our rule) get { scope: "account" }, the admin view of every website.
- [ ] F-4. Respond { url } with Cache-Control: no-store. Never store or log the url: it works once, within 60 seconds.
- [ ] F-5. A company with no websiteId yet gets one on the spot (C-1) and the request continues.
## G. The page (browser)
- [ ] G-1. A "Website visitors" page (our navigation, a full-height borderless iframe) and an "Install tracking" item opening view=install; one iframe per screen.
- [ ] G-2. Fetch a fresh url every time a screen is shown; never reuse one.
- [ ] G-3. (Optional: sessions last as long as we set in Embed settings, 1 week by default) on postMessage "session-ended" (check e.origin === new URL(url).origin) load a fresh url, at most every 30 seconds.
- [ ] G-4. No key, master or embed, in any front-end file or bundle: grep the build output to prove it.
- [ ] G-5. White label: nothing our users can see says Happierleads. No brand name in our routes or URLs (use /api/leads-embed), page or iframe titles, menu items, error messages, front-end code or logs shown to users. Use our own product words (e.g. "Website visitors").
## H. Security and tests
- [ ] H-1. Every destructive Happierleads call (e.g. DELETE /admin/websites/{id}) runs only on our server after our own permission check; none from the browser.
- [ ] H-2. Tests: the client module (mocked fetch), sign-up creating and saving the website, the migration run twice creates no duplicates, billing paid and cancelled, the embed endpoint (401 signed out, only the user's own websiteId even when another is sent, staff scope, no-store header).
- [ ] H-3. End to end on staging, with me: sign up a test client → the install screen opens in the iframe; log in again → the leads screen.
Match this project's code style. Work through the checklist in order and keep INTEGRATION_CHECKLIST.md current after every change.