Skip to main content
You create the survey in Keep (Feedback → Surveys), publish it, and install the widget in your app. When someone answers, the answer reaches Keep and shows up next to the account it came from — MRR, renewal, risk and usage, read from your integrations when you look, never sent to the browser. Keep shows these side by side: “the account is at risk and gave a 3”. It never claims one caused the other.

Survey models

Every model can ask an optional follow-up question after the score.

Install

Paste this in the pages where the survey may appear:
The script loads asynchronously and never blocks your page. Calls made before it loads are queued and run when it arrives. It is about 8 KB gzipped.
The widget draws inside a Shadow DOM: your app’s CSS does not affect the survey, and the survey’s CSS does not leak into your app. It works under a Content Security Policy without 'unsafe-inline' in style-src — allow only https://usekeep.dev in script-src and connect-src.

Methods

function
surveyId (or surveys: ["…", "…"]) — the surveys this page may show. debug: true logs to the console why a survey did not appear.
function
Who is using your app. Every field is optional: userId, email, companyId, customerId (the customer’s id in your billing, like Stripe’s cus_… — it makes the match to the account exact), plan, createdAt (ISO 8601, for “new users” targeting) and signature (see below).
function
Show it now. Used by the Manual (SDK) trigger, and any time you want.
function
Fires the Custom event trigger: Keep.track("checkout_completed") shows the surveys set up for that event.
function
Forgets the user. Call it on logout.

Triggers and audience

Triggers: right away, after N seconds, after N page views, manually via Keep.show, or on a custom event via Keep.track. Audience: everyone, identified users only, new users (signed up in the last N days), specific plans, or specific companies. Plan and company are the values your app passes to Keep.identify. The audience is evaluated on Keep’s server, which answers only “show” or “don’t show” — the list of plans or companies never reaches the browser. After someone answers or closes the survey, it is not shown to them again for the number of days you set (90 by default).

Verified identity

Anything the browser sends can be typed by anyone, so identity has two layers:
  • Unsigned: the answer is stored with what your app said and marked not verified in Keep.
  • Signed: your server signs the user with the identity secret (Feedback → Settings). The answer arrives verified.
The signature is the HMAC-SHA256, in hex, of the userId — or of the email when there is no userId:
With Require verified identity on, answers without a valid signature are stored as anonymous. The secret must never reach the browser.

How an answer is linked to an account

Strongest first:
  1. The customerId your app passed, matched exactly to a customer in your billing.
  2. The respondent’s email, matched exactly to the account’s email.
  3. The company domain of the respondent’s email, when exactly one account has it — and never a public mailbox like gmail.com.
Answers that match nothing are kept and shown as customer not identified.

React and Next.js

Inline surveys

With the Inline display, place where it should appear:

What the widget receives and sends

  • It receives only what it needs to draw the survey: texts, scale, look and trigger. Nothing about your accounts, revenue, risk or audience.
  • It sends the score, the comment, the identity you passed, and the page path — without the query string, which is where tokens tend to live.
  • It uses no cookies. It keeps in localStorage only that someone answered or closed, so it does not ask again too soon.
  • Any failure is silent: a paused or deleted survey, a network error — your page stays as it was.
  • Survey texts are always rendered as text, never as HTML.

Endpoints

Used by the widget; you do not need to call them. Limits: a whole score on the survey’s scale, a comment of up to 2,000 characters, and 10 answers per hour per survey per address. With Allowed sites set in Feedback → Settings, requests from other origins are refused. Pausing a survey takes up to a minute to reach every page, because the configuration is cached briefly.

Testing locally

Use Keep.init({ surveyId, debug: true }). The builder’s preview in Keep uses this exact widget and never sends answers.