Skip to content
Console

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.

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.

KindLooks likeGood for
emoji3three faces 🙁 😐 🙂a quick mood read
scale7a 1–7 scalea finer NPS-style score
thumbs👍 / 👎a plain yes/no

Pick one per project. Each vote flows into the same Satisfaction view.

The survey is off by default on every project. You enable it in the console:

  1. Open Project Settings → Feedback.
  2. Pick the kind — emoji3, scale7, or thumbs.
  3. 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.

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>

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.

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(): 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(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:

Kindvalue
emoji30 (sad), 1 (neutral) or 2 (happy)
scale71 to 7
thumbs0 (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:

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

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.

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.

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_... and content-type: application/json. The browser sends Origin, 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" }. kind and value are required, with the ranges above. page_host (up to 255 characters) and user_ref (up to 400) are optional strings. The body can be up to 16 KB.
  • Answer: 200 with { "success": true, "data": { "vote_id": "<32 hex characters>" } }.
StatusMessageCause
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.
403Missing x-espejo-key header. or Invalid ingest key.No key, or an unknown or revoked one.
403This account is deactivated. Recording is off.The account is deactivated.
403Origin <origin> is not allowed for this project.The Origin is not in the allowed list.
403Feedback is disabled for this project.The survey is off in Project Settings → Feedback.
403This project hit its daily feedback vote cap. It resets at midnight UTC.The project reached 50,000 votes today.
503Ingest is unavailable: the database is not configured.The service is unavailable.
  • 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.