- Guides
- Feedback (CSAT)
Feedback (CSAT)
A micro-survey in three kinds (emoji3, scale7, thumbs), shown once per visitor per cadence window. It is off by default, and viewing results is Pro.
Feedback is a micro-survey Espejo shows to your end users. One tap, one vote, and it lands on the Satisfaction dashboard. It is the cheapest way to ask “how was that?” without building a survey tool of your own.
Two widgets, don’t confuse them
Section titled “Two widgets, don’t confuse them”Espejo ships two separate things, and they go to two different places:
- The CSAT survey — the little question below — is a vote. It goes to Satisfaction.
- The “Report a problem” button (🐞) is a recording. It goes to Recordings.
One measures how people feel; the other captures what went wrong. They are enabled and read independently.
The three kinds
Section titled “The three kinds”| Kind | Looks like | Good for |
|---|---|---|
emoji3 | three faces 🙁 😐 🙂 | a quick mood read |
scale7 | a 1–7 scale | a finer NPS-style score |
thumbs | 👍 / 👎 | a plain yes/no |
Pick one per project. Each vote flows into the same Satisfaction view.
Turning it on
Section titled “Turning it on”The survey is off by default on every project. You enable it in the console:
- Open Project Settings → Feedback.
- Pick the kind —
emoji3,scale7, orthumbs. - Set
cadence_days— how long before the same visitor is asked again.
Nothing changes in your page; the SDK reads the project’s config. A visitor sees the survey once per cadence window, and the “seen” stamp is written when it appears — not when they answer — so someone who dismisses it is not pestered again until the window rolls over.
Turning it off from the page
Section titled “Turning it off from the page”The project config is the default. To suppress the survey on a specific page or build, set the attribute on the script:
<script src="https://app.espejo.dev/sdk/espejo.js" data-key="pk_live_..." data-feedback="off"></script>Your own feedback form
Section titled “Your own feedback form”When the survey has to match your design, or appear at a moment you choose, draw it yourself and send the votes with the SDK. They land on the same Satisfaction dashboard as the widget’s. The survey still has to be on in Project Settings → Feedback: that is where the project picks its kind, and a project with the survey off rejects votes.
Turn off Espejo’s widget
Section titled “Turn off Espejo’s widget”So your visitors do not get two surveys, turn ours off on the pages that show
yours. With npm, pass feedback: "off" to the constructor:
import { Espejo } from "@espejo/browser";
const espejo = new Espejo({ key: "pk_live_...", feedback: "off" });espejo.start();With the script tag, add data-feedback="off", as shown in
Turning it off from the page. Either way only
the widget goes away: getFeedbackConfig() and vote() keep working.
getFeedbackConfig()
Section titled “getFeedbackConfig()”getFeedbackConfig(): Promise<FeedbackConfig | null>
interface FeedbackConfig { enabled: boolean; // always true when the config is not null kind: "emoji3" | "scale7" | "thumbs"; cadence_days: number; // 1 to 365}It returns the project’s survey config, or null when the survey is off. It
also returns null when the config cannot be loaded (no network, an error
from the API, or no answer within 2 seconds). It never throws. Use it to
decide whether to show your form and which question to draw. The browser can
cache the answer for up to 5 minutes, so a change in the console can take
that long to reach an open page.
FeedbackConfig and FeedbackKind are exported as types from
@espejo/browser.
vote()
Section titled “vote()”vote(value: number, options?: { kind?: FeedbackKind }): Promise<void>It sends one vote and resolves once the API accepts it. value is the raw
answer, an integer in the range of its kind:
| Kind | value |
|---|---|
emoji3 | 0 (sad), 1 (neutral) or 2 (happy) |
scale7 | 1 to 7 |
thumbs | 0 (thumbs down) or 1 (thumbs up) |
options.kind defaults to the project’s kind. vote() also sends the host of
the current page and your userRef (from the constructor, data-user-ref or
identify()), like the widget does.
It rejects with an Error in these cases:
| Case | Message starts with |
|---|---|
| The survey is off, or its config could not be loaded. | Espejo: feedback is off |
value is not an integer in the range of the kind. Nothing is sent. | Espejo: invalid <kind> vote |
| The API answered with a status outside 2xx. | ingest rejected the vote: HTTP <status>, followed by the start of the response body |
A failed network request rejects with the browser’s own error.
Cadence is yours
Section titled “Cadence is yours”vote() does not apply cadence_days and does not remember who already
answered. Deciding when to ask, and how often, is up to your code. The config
gives you the project’s cadence_days if you want to follow it.
A complete example
Section titled “A complete example”Three emoji buttons that show up only when the survey is on, and at most once
every cadence_days per browser.
<div id="csat" hidden> <p>How was your experience?</p> <button data-value="0">🙁</button> <button data-value="1">😐</button> <button data-value="2">🙂</button></div>import { Espejo } from "@espejo/browser";
const espejo = new Espejo({ key: "pk_live_...", feedback: "off" });espejo.start();
const LAST_ASKED = "my-app:csat-asked-at";const DAY_MS = 24 * 60 * 60 * 1000;
async function maybeAsk() { const config = await espejo.getFeedbackConfig(); if (!config || config.kind !== "emoji3") return;
const last = Number(localStorage.getItem(LAST_ASKED) ?? 0); if (Date.now() - last < config.cadence_days * DAY_MS) return; localStorage.setItem(LAST_ASKED, String(Date.now()));
const box = document.getElementById("csat")!; box.hidden = false; box.querySelectorAll<HTMLButtonElement>("button").forEach((button) => { button.addEventListener("click", async () => { box.hidden = true; try { await espejo.vote(Number(button.dataset.value)); } catch (error) { console.warn("The vote was not saved", error); } }); });}
maybeAsk();With the script tag, the same instance is window.espejo, with the same
getFeedbackConfig() and vote():
<script src="https://app.espejo.dev/sdk/espejo.js" data-key="pk_live_..." data-feedback="off"></script><script> window.espejo.getFeedbackConfig().then((config) => { if (!config) return; // Show your form, then on a click: // window.espejo.vote(2).catch((error) => console.warn(error)); });</script>window.espejo exists once espejo.js has run. Load it without async or
defer if an inline script right after it uses it, or wait for the load
event.
The HTTP endpoint
Section titled “The HTTP endpoint”For a form that does not use the SDK. Call it from the visitor’s browser.
Config: GET https://app.espejo.dev/v1/ingest/feedback-config?key=pk_live_...
always answers 200 with
{ "success": true, "data": { "enabled", "kind", "cadence_days" } }. An
unknown or revoked key, a deactivated account and a survey that is off all
return enabled: false.
Vote: POST https://app.espejo.dev/v1/ingest/feedback
- Headers:
x-espejo-key: pk_live_...andcontent-type: application/json. The browser sendsOrigin, and it must be one of the project’s allowed origins (a project with no allowed origins accepts any). Send it without cookies (credentials: "omit"). - Body:
{ "kind": "emoji3", "value": 2, "page_host": "app.example.com", "user_ref": "user-123" }.kindandvalueare required, with the ranges above.page_host(up to 255 characters) anduser_ref(up to 400) are optional strings. The body can be up to 16 KB. - Answer:
200with{ "success": true, "data": { "vote_id": "<32 hex characters>" } }.
| Status | Message | Cause |
|---|---|---|
400 | `kind` must be one of: emoji3, scale7, thumbs. | kind is missing or unknown. |
400 | `value` is out of range for this feedback kind. | value is not an integer in the range of kind. |
403 | Missing x-espejo-key header. or Invalid ingest key. | No key, or an unknown or revoked one. |
403 | This account is deactivated. Recording is off. | The account is deactivated. |
403 | Origin <origin> is not allowed for this project. | The Origin is not in the allowed list. |
403 | Feedback is disabled for this project. | The survey is off in Project Settings → Feedback. |
403 | This project hit its daily feedback vote cap. It resets at midnight UTC. | The project reached 50,000 votes today. |
503 | Ingest is unavailable: the database is not configured. | The service is unavailable. |
Limits
Section titled “Limits”- A vote is one number. There is no free text comment: the API has no field for one.
- Up to 50,000 votes per project per day. The count resets at midnight UTC.
- One of the three kinds per project, chosen in the console.