Getting started
Feedzap is a single 14 KB script tag. Drop it in and three things start working immediately:
- Feedback widget — users click a floating pill, mark a broken element, leave a note; you get a report with a screenshot, the DOM selector, the URL and the browser context.
- Passive behavioral signals — rage clicks, dead clicks, form abandonment, JS errors and slow API calls get captured silently in the background.
- Friction Map — the dashboard ranks the most painful pages and elements on your product.
To get the Churn Risk dashboard working, add one extra line:
Feedzap.identify({ id: user.id, email: user.email, plan: user.plan, mrr: user.mrr });That's it. The full path is:
- Create a free account — magic link or Google, no card.
- Create a project. You land on the install page with a snippet pre-filled with your project key.
- Paste the snippet into your site's
<head>(or just before</body>). - Call
Feedzap.identify(...)from your login flow so churn detection can attribute signals to real users. - Watch reports, signals and churn scores land in your dashboard in real time.
Install the widget
Drop this snippet just before </body>. Replace YOUR_PROJECT_KEY with the key from your project's install page.
<script
src="https://www.feedzap.live/widget/feedzap.js"
data-project-id="YOUR_PROJECT_KEY"
data-api-url="https://www.feedzap.live"
async
></script>The widget is ~14 KB gzipped, loads async, mounts in a Shadow DOM (so your styles never collide with ours), and respects prefers-reduced-motion. No third-party cookies. Works on every modern browser.
Next.js
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Script
src="https://www.feedzap.live/widget/feedzap.js"
data-project-id="YOUR_PROJECT_KEY"
data-api-url="https://www.feedzap.live"
strategy="afterInteractive"
/>
</body>
</html>
);
}React / Vite
Add the script tag to index.html just before </body> — same snippet as the HTML example above. No npm install needed.
npm package
If you'd rather bundle the widget yourself:
npm install @feedzap/widget
// In your entry file
import { Feedzap } from "@feedzap/widget";
Feedzap.init({
projectId: "YOUR_PROJECT_KEY",
apiUrl: "https://www.feedzap.live",
});Verify the install
Open your site and look for the floating Feedzap pill in the bottom-right. Your project's install page also shows a green “Connected” badge the first time the widget pings home.
Identify your users
Powers Churn RiskOut of the box, every browser that loads your site gets an anonymous ID stored in localStorage. That's enough for the Friction Map and Signals tab, but it isn't enough to flag “Sarah from Acme is about to churn.” For that, Feedzap needs to know who the browser belongs to.
Call Feedzap.identify() once, anywhere you already have the logged-in user object. Every prior anonymous signal from this browser gets stitched onto that profile automatically.
The call
Feedzap.identify({
id: user.id, // your internal user ID — required
email: user.email, // surfaced in the dashboard + the "Reach out" button
plan: user.plan, // "Free" | "Pro" | "Team" | "Enterprise"…
mrr: user.mrr, // monthly revenue in dollars (number)
context: { // anything else — shown in the report drawer
company: user.company,
signupDate: user.createdAt,
role: user.role,
},
});Where to put it
- Right after login. Inside your auth callback, once the session is hydrated.
- On every page for logged-in users. A
useEffectat the root of your app that fires whenuserchanges is the simplest pattern. - In your server-rendered shell. If you SSR the user object, you can render the call inline so it fires before any interaction.
React example
// _app.tsx or root layout client wrapper
"use client";
import { useEffect } from "react";
export function FeedzapIdentify({ user }) {
useEffect(() => {
if (!user) return;
window.Feedzap?.identify({
id: user.id,
email: user.email,
plan: user.subscription?.plan,
mrr: user.subscription?.mrr,
});
}, [user]);
return null;
}What identify actually does
- Writes your identity payload into the widget's in-memory state and into
localStorageso subsequent page loads remember it. - Attaches
userIdandanonymousIdto every feedback report and to every behavioral signal batch the widget sends. - Server-side, upserts a row in
user_profileskeyed on(project_id, user_id). If an anonymous profile already existed for this browser, the two rows get stitched. - Keeps
last_seen_atfresh every 10s while the user is browsing — even on pages with zero clicks — so “went silent for 7 days” is accurate.
Clearing identity on logout
Feedzap.reset(); // forgets identity, generates a fresh anonymous IDCall this in your logout handler so the next person on a shared browser doesn't inherit the previous user's identity.
Behavioral signals (automatic)
Zero configThe widget passively detects nine kinds of friction without you writing a single line of tracking code. Everything below starts firing the moment the script tag loads.
| Signal | What it means |
|---|---|
rage_click | 3+ clicks on the same element within 1s — the universal “why isn't this working” signal. |
dead_click | Click on an element that does nothing — looks interactive but isn't wired up. |
rage_scroll | Repeated fast scroll reversals — user can't find what they're looking for. |
slow_interaction | A click whose handler took >3s to respond. Tells you which buttons feel laggy. |
form_abandonment | User typed into a form, then left the page without submitting. |
error_encounter | An on-page error toast or error UI was visible when the user interacted. |
js_exception | An uncaught JavaScript exception fired in the user's tab. |
slow_api | A fetch / XHR that took >3s. Mapped to the page the user was on at the time. |
network_error | A fetch / XHR that returned 4xx or 5xx (or failed outright). |
How they're sent
Signals get queued in memory and flushed every 10s as a single batch request — one HTTP call per minute, not one per signal. On pagehide and visibilitychange, a final sendBeacon flush fires so nothing is lost.
Privacy
No keystrokes, no form values, no cursor recording, no session replay. Selectors are CSS paths (e.g. button[data-cta=pay]), not screenshots. The widget never reads anything inside an <input> or <textarea>.
Churn Risk Detection
PremiumChurn Risk is the headline output of everything above. Open Analytics → Churn Risk in any project and you'll see every identified user scored 0–100, sorted by who's most likely to cancel.
The seven signals we score
Each user's last 14 days are scanned against seven pre-churn patterns. Points stack — raw scores cap at 160, then get normalised to 0–100.
| Signal | Points | What it means |
|---|---|---|
Filed a bug then went silent | +30 | Submitted feedback, then no further activity for 7+ days. The single strongest pre-churn pattern we see. |
3+ bug reports in 14 days | +30 | Someone hitting submit on the widget repeatedly is either deeply engaged or deeply frustrated. Usually the latter. |
Expressed frustration 2+ times | +25 | Picked the “Frustrating” reaction on multiple reports. |
Hit the same broken element repeatedly | +20 | Two or more reports on the same DOM selector — they keep trying the thing that doesn't work. |
Visited billing / upgrade / cancel pages | +20 | Looked at how to leave (or how much they're paying). |
5+ JS errors or network crashes | +20 | Their experience is broken even if they never told you. |
Rage-clicked 3+ times without reporting | +15 | Silently frustrated. Worse than a bug report — they've given up on telling you. |
Score bands
Likely to cancel in the next 14 days. Reach out personally this week.
Showing 2+ pre-churn signals. Worth a check-in email or a fix on the broken thing.
Either silent or actively engaged with no friction. Filtered out of the main list.
What the dashboard shows
- A red banner with the count of at-risk users and the total MRR exposed (or a green all-clear if everyone is healthy).
- A list of users sorted by score, each with their email, plan, MRR, the primary reason they're flagged, signal chips, last-seen time, and a 14-day activity sparkline.
- A drawer per user with the full breakdown: every signal that fired with its exact point contribution, a Reach out button that opens your mail client pre-filled with their email, and a Mark as handled button that snoozes them for 14 days.
- A “What we're watching” grid of all seven signal cards with live counts.
Snooze behaviour
“Mark as handled” hides the user from the list for 14 days. Their score still recomputes in the background — if a new signal fires (e.g. they file another bug), the snooze stays in effect until it expires. Useful for “I already called them, stop nagging me.”
Refresh cadence
Scores are recomputed on demand and cached for 1 hour. Open the tab, see the latest. The compute reads from your feedback, behavioral signals and user profiles tables — no separate event log.
Feedzap.identify() yet, the tab shows a “Listening for signals” empty state. See Identify your users.Friction Map
The Friction Map sits on the Overview tab of Analytics. It ranks every page and every element on your product by a composite friction score combining:
- Volume of feedback reports
- Rage-click and dead-click density
- JS exceptions and slow-API hits attributed to that page
- The “Frustrating” reaction rate
Pages and elements bubble up in a single sorted list so the noisiest area of your product is always at the top — no manual digging. Click any row to jump straight to the reports and signals that contributed to its score.
Free plan can view the Friction Map. The Execute Fix actions on the Signals tab are Premium-only.
Sentry & Datadog
OptionalIf your team already runs Sentry or Datadog RUM, you can pipe their issue feed into Feedzap so JS exceptions and slow APIs show up as behavioral signals against the right user.
Sentry
- Open Project → Settings → Signal sources.
- Paste a Sentry auth token, your org slug and your project slug.
- Feedzap polls the Sentry REST API every hour for unresolved issues from the last 24h and upserts them as
js_exceptionrows. One Sentry issue = one signal row per day — safe to re-run.
Datadog
- In the same settings panel, paste a Datadog API key + app key.
- Feedzap pulls slow-API and 5xx events from Datadog RUM into
slow_apiandnetwork_errorsignals.
Tokens are encrypted at rest. You can disconnect either source any time — historical signals stay, polling stops.
API reference
If you can't use the snippet (server-side flows, native apps, CLIs, backend bots), POST reports directly to the ingest endpoint.
POST /api/ingest
Multipart form data. The screenshot field is a binary JPEG blob; everything else is a string.
curl -X POST https://www.feedzap.live/api/ingest \
-F "publicKey=YOUR_PROJECT_KEY" \
-F "message=Checkout button is dead on Safari 17" \
-F "pageUrl=https://yourapp.com/checkout" \
-F "selector=button[data-cta=pay]" \
-F "reaction=bug" \
-F "userId=user_123" \
-F "userEmail=ada@example.com" \
-F "userPlan=pro" \
-F "userMrr=49" \
-F "anonymousId=abc-123-def" \
-F "viewportW=1440" \
-F "viewportH=900" \
-F "clickX=820" \
-F "clickY=410" \
-F "userAgent=Mozilla/5.0..." \
-F "screenshot=@/path/to/screen.jpg"POST /api/projects/:id/signals/batch
JSON body. Send up to 50 events in one batch. Each event is individually validated; invalid events are dropped and the rest are accepted — the response tells you exactly how many of each.
POST /api/projects/<projectId>/signals/batch
Content-Type: application/json
{
"publicKey": "YOUR_PROJECT_KEY",
"anonymousId": "abc-123-def",
"userId": "user_123",
"userEmail": "ada@example.com",
"userPlan": "pro",
"userMrr": 49,
"events": [
{
"eventType": "rage_click",
"selector": "button[data-cta=pay]",
"pageUrl": "https://yourapp.com/checkout",
"sessionId": "sess_01H...",
"timestamp": "2026-05-27T12:34:56Z",
"metadata": { "clickCount": 5 }
}
]
}Responses
200—{ ok, id, similarCount }for ingest, or{ ok, accepted, dropped }for the signal batch.400— invalid payload. The response includes a Zod error breakdown.402— monthly limit hit (free plan, 20 feedback reports). Upgrade to Premium for unlimited.404— project key not found.413— request too large (>8 MB total / >6 MB screenshot).500— database or storage error.
CORS
Both endpoints answer OPTIONS with Access-Control-Allow-Origin: *. The widget runs on arbitrary customer origins — you don't need to whitelist anything.
Slack & Discord
Get a Slack or Discord ping the moment a report lands. Add the webhook URL under Project → Settings → Integrations — we'll auto-detect which service it's for based on the host.
What gets sent
- A header line with the reaction emoji (🐛 / 💡 / 😤 / ❤️) and the project name.
- The message body, truncated to a sensible length.
- Page URL, DOM selector, tags — whichever are present.
- A View in Feedzap button that deep-links into the inbox.
Failures are silent
If Slack / Discord returns an error, the customer's feedback submission still succeeds — we log the failure server-side and move on. Notifications never block ingestion.
Webhooks
Pipe new reports into your own endpoint (Linear, Jira, a custom queue, anything). Add a webhook URL under Project → Settings → Webhooks.
Payload
POST <your-webhook-url>
Content-Type: application/json
{
"event": "feedback.created",
"feedback": {
"id": "fb_01H...",
"project_id": "proj_01H...",
"message": "Checkout button is dead",
"page_url": "https://yourapp.com/checkout",
"selector": "button[data-cta=pay]",
"reaction": "bug",
"tags": ["broken", "checkout"],
"user_agent": "Mozilla/5.0...",
"created_at": "2026-05-27T12:34:56Z",
"view_url": "https://www.feedzap.live/p/proj_01H.../inbox"
}
}Delivery semantics
- Fire-and-forget after the report is durably saved. Your endpoint has a 5-second timeout to respond.
- No retries currently — we log failed deliveries server-side. If you need at-least-once, accept fast and enqueue on your side.
- All Linear and Jira integrations live inside Feedzap; you don't need a webhook for those — configure them directly under Settings → Integrations.
Plans & limits
- Free — 20 feedback reports / month, unlimited projects, unlimited teammates, Friction Map, Signals tab (view-only).
- Premium — $10 / month — unlimited reports, AI auto-insights, Churn Risk Detection, Execute Fix on the Signals tab, priority support.
Usage resets on the 1st of each month. Billing is per project via Razorpay — cancel from settings in one click. See pricing for the full comparison table.
Team & invites
Invite teammates from Project → Team. They get a magic-link invite via email — seats are unlimited on both plans.
Roles: owner (billing + delete project), admin (settings + invites + integrations), member (inbox + insights, no settings).
Still stuck?
We answer every email. Reach out and we'll help you ship — usually inside the hour.
Get started free