Feedzap
Documentation

Ship Feedzap in 5 minutes.

Paste one snippet. Out of the box you get the feedback widget, passive rage-click and error tracking, the Friction Map, and the Churn Risk dashboard. One extra line of code — Feedzap.identify() — turns anonymous signals into named users you can call before they cancel.

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:

  1. Create a free account — magic link or Google, no card.
  2. Create a project. You land on the install page with a snippet pre-filled with your project key.
  3. Paste the snippet into your site's <head> (or just before </body>).
  4. Call Feedzap.identify(...) from your login flow so churn detection can attribute signals to real users.
  5. 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 Risk

Out 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 useEffect at the root of your app that fires when user changes 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 localStorage so subsequent page loads remember it.
  • Attaches userId and anonymousId to every feedback report and to every behavioral signal batch the widget sends.
  • Server-side, upserts a row in user_profiles keyed on (project_id, user_id). If an anonymous profile already existed for this browser, the two rows get stitched.
  • Keeps last_seen_at fresh every 10s while the user is browsing — even on pages with zero clicks — so “went silent for 7 days” is accurate.
If you skip identify, the Churn Risk tab will show a “Listening for signals” empty state. The rest of Feedzap (widget, signals, friction map) still works perfectly — you just won't know who is rage-clicking.

Clearing identity on logout

Feedzap.reset();   // forgets identity, generates a fresh anonymous ID

Call 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 config

The 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.

SignalWhat it means
rage_click3+ clicks on the same element within 1s — the universal “why isn't this working” signal.
dead_clickClick on an element that does nothing — looks interactive but isn't wired up.
rage_scrollRepeated fast scroll reversals — user can't find what they're looking for.
slow_interactionA click whose handler took >3s to respond. Tells you which buttons feel laggy.
form_abandonmentUser typed into a form, then left the page without submitting.
error_encounterAn on-page error toast or error UI was visible when the user interacted.
js_exceptionAn uncaught JavaScript exception fired in the user's tab.
slow_apiA fetch / XHR that took >3s. Mapped to the page the user was on at the time.
network_errorA 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

Premium

Churn 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.

SignalPointsWhat it means
Filed a bug then went silent+30Submitted feedback, then no further activity for 7+ days. The single strongest pre-churn pattern we see.
3+ bug reports in 14 days+30Someone hitting submit on the widget repeatedly is either deeply engaged or deeply frustrated. Usually the latter.
Expressed frustration 2+ times+25Picked the “Frustrating” reaction on multiple reports.
Hit the same broken element repeatedly+20Two or more reports on the same DOM selector — they keep trying the thing that doesn't work.
Visited billing / upgrade / cancel pages+20Looked at how to leave (or how much they're paying).
5+ JS errors or network crashes+20Their experience is broken even if they never told you.
Rage-clicked 3+ times without reporting+15Silently frustrated. Worse than a bug report — they've given up on telling you.

Score bands

High risk70 – 100

Likely to cancel in the next 14 days. Reach out personally this week.

Medium risk40 – 69

Showing 2+ pre-churn signals. Worth a check-in email or a fix on the broken thing.

Low / healthy0 – 39

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.

Prerequisite: Churn Risk only scores identified users. If you haven't called 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

Optional

If 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

  1. Open Project → Settings → Signal sources.
  2. Paste a Sentry auth token, your org slug and your project slug.
  3. Feedzap polls the Sentry REST API every hour for unresolved issues from the last 24h and upserts them as js_exception rows. One Sentry issue = one signal row per day — safe to re-run.

Datadog

  1. In the same settings panel, paste a Datadog API key + app key.
  2. Feedzap pulls slow-API and 5xx events from Datadog RUM into slow_api and network_error signals.

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.

402 behaviour: when a free project hits 20 reports, the widget keeps loading on your site (visitors see nothing broken) but new submissions return 402 until the month rolls over.

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