Heresay docs
Everything you need to add a Report button to your app, read what people say, and answer them.
Set up Heresay
Each team runs its own Heresay, in its own Google account, so your reports never sit on anyone else's servers. One command sets it up:
npx create-heresay
It signs you in to Google, creates a Firebase project, links a billing account, deploys the dashboard, API and SDK, and makes you the owner. It takes about five minutes, and you can run it again if anything stops partway. Firebase's pay-as-you-go plan is needed for the API; a small app stays inside its free tier.
Later, npx create-heresay update deploys new versions and npx create-heresay status shows what's running.
Add it to an app
- Open your dashboard at
https://<your-heresay>.web.app/app/(create-heresayprints the address) and choose Add an app. Pick the platform, give it a name, and list the websites it runs on, for examplehttps://app.example.comandhttp://localhost:3000. - Copy the line the dashboard shows you (it has steps for Next.js, Nuxt and others too) and paste it before
</body>on every page:
<script src="https://<your-heresay>.web.app/sdk/v1.js" data-key="pk_your_key" defer></script>
- Reload your app. A Report button appears in the bottom corner. Send yourself a test report and the dashboard shows it within a few seconds.
That's the whole setup. Everything below is optional.
What your users see
A small Report button in the corner. Tapping it opens a panel with two tabs:
- Report: pick what it is (Broken, Confusing, Could be better, Idea), write a sentence, and send. The current page and app version are attached automatically.
- Your reports: everything they've sent from this device and what happened to it. When you answer, a dot appears on the button until they've seen it.
Nobody signs up. Each browser gets a random device id, which is how people see their own reports and nobody else's.
The dashboard
Reports arrive sorted by urgency: open reports first, then Broken, Confusing, Could be better and Idea, oldest first within each. For each report you can:
| Action | What happens |
|---|---|
| Accept | The reporter sees "Accepted, being worked on". The report becomes available as a coding-agent prompt. |
| Decline… | You must write a reason. The reporter reads it, so write it for them. |
| Mark fixed… | For accepted reports. The reporter sees "Fixed", and your note if you write one. |
| Copy prompt for agent | For accepted reports. See Coding agents. |
Open an app and choose Install to see its line again. Everyone on the Team page can read and answer reports for every app; owners can add and remove people. People sign in with Google or an email link, using the address an owner added.
Script tag options
Add any of these attributes to the script tag.
| Attribute | Example | What it does |
|---|---|---|
data-key | pk_… | Required. Your app's key from the dashboard. |
data-version | 1.4.0 | Your app's version, attached to every report. |
data-user-id | u_1842 | Who the user is, in your system. |
data-user-label | Asha K. | A name to show in your dashboard. |
data-user-email | asha@example.com | Their email, so you can reply. Or use identify below. |
data-accent | #6d28d9 | Your brand colour for the Send button, selected states and the logo. Hex only. Text on it switches to white or black automatically. |
data-position | left | Put the button in the bottom-left corner instead of the right. More in Look and placement. |
data-api | https://…/v1 | Rarely needed. Where your Heresay API lives, if it is not at the same address as sdk.js. |
<script src="https://<your-heresay>.web.app/sdk/v1.js"
data-key="pk_your_key"
data-version="1.4.0"
data-accent="#6d28d9"
defer></script>
Look and placement
The defaults are what we recommend: change only what your app needs. The easiest way is the dashboard: open an app and choose Design to try each option on the real widget and copy the tag. The first value is the default.
| Attribute | Values |
|---|---|
data-position | right · left · top-right · top-left · center |
data-offset | 20 px from the edges, or x,y like 24,96 to clear a chat bubble |
data-button | always · none (open it from your own menu with Heresay.open()) · desktop (not on phones) · scroll (after the first scroll) |
data-label | Report, or your own words up to 40 characters |
data-style | pill · icon · tab (on the side edge) |
data-size | regular · small · large |
data-fill | neutral · accent (filled with your colour) |
data-shadow | soft · none · strong |
data-hide-on | paths where the button stays hidden, like /checkout,/login |
data-mark | accent (the logo follows your colour) · heresay (keep the Heresay teal) |
data-theme | auto (follows the device) · light · dark |
data-font | system · inherit (your page's font) |
data-lang | your page's <html lang> · auto (the browser's) · en · fr · ta (Tamil) · hi (Hindi) |
data-placeholder | the question in the text box |
data-types | all four, or some of broken,confusing,improvement,idea. Their names don't change: the type sets the priority. |
data-thanks | a line shown after sending |
data-panel | corner · sheet (full height on that side) · center |
data-width | regular · narrow · wide |
data-backdrop | dim · clear · blur |
data-preferences | show · hide |
Always there, whatever you choose: the Heresay mark on the button, the Your reports tab (people read why a report was declined), and Powered by Heresay in the panel and the introduction, so people can tell your app uses an outside tool. The widget lives in a shadow root, so your CSS can't restyle it.
JavaScript API
The script creates window.Heresay (also available as window.Feedback). Call these any time after it loads.
| Call | What it does |
|---|---|
Heresay.identify({ id, label, email }) | Tell Heresay who is signed in. Call it after login; pass nothing to clear it. Then nobody is asked their name to send a report, and with email you can reply to them. Apps with many users and logins need nothing more: it follows whoever is signed in on that device. |
Heresay.setVersion("1.4.0") | Set or change the app version. |
Heresay.setScreen("Checkout") | Name the current screen, for apps whose URL doesn't change between screens. |
Heresay.open({ type, text }) | Open the report form from your own button or menu item. type and text are optional and fill it in, for example from an error screen; the person still reviews and sends it. |
Heresay.on("sent", fn) | Calls fn({ id, type }) after each report is sent, to thank people or count it in your analytics. Returns a function that stops it. |
Heresay.introduce({ title, body }) | Once per device, a small bubble over the button says what it's for, so people know it exists. Call it when your main screen is up (after sign-in or onboarding), or add data-intro="auto" to the tag. Later calls do nothing. title and body are optional, to use your own words. |
Heresay.openPreferences() | Open the Preferences tab: a note about their setup, sent with every report. People who aren't signed in can also add a name and an email for replies. It's all optional and saved on their device. Where the button sits is yours to choose, with data-position. |
Heresay.close() | Close the panel. |
Each report also carries the page it came from: its title, its address without the query string, and the window size.
// after your user signs in
Heresay.identify({ id: user.id, label: user.name, email: user.email });
// single-page apps: name screens as the user moves
router.afterEach((to) => Heresay.setScreen(to.name));
Report types and statuses
Types, picked by the reporter
| Type | Means | Priority |
|---|---|---|
| Broken | Something doesn't work | 1 · first |
| Confusing | It works, but they couldn't tell how | 2 |
| Could be better | It works, and could be better | 3 |
| Idea | Something that doesn't exist yet | 4 |
Reporters never pick a priority. It comes from the type. Confusing ranks above Could be better on purpose: a feature nobody can operate is closer to broken.
Statuses
| Dashboard | What the reporter sees |
|---|---|
| Open | Waiting for the developer |
| Accepted | Accepted, being worked on |
| Fixed | Fixed |
| Declined | Declined, with your reason |
Coding agents
Heresay can hand accepted reports to a coding agent (Claude Code, Cursor, Codex, anything with MCP or a shell). In your app's repo, run:
npx heresay connect --url https://<your-heresay>.web.app
It opens your dashboard to make a token for that repo, then adds three files you commit: .mcp.json (the Heresay MCP server), .claude/skills/heresay/SKILL.md (a short skill) and a section of AGENTS.md. The token itself stays in ~/.heresay on your computer, never in the repo. Then ask your agent:
- "Add Heresay to this app": it creates the app if needed, puts the tag in the right place for your framework, and waits for the first report.
- "Fix the next Heresay report": it picks an accepted brief, claims it, fixes it on a branch, and marks it fixed with a note the reporter reads.
What an agent can and can't do
| Can | Can't |
|---|---|
| List, read, claim and add notes to accepted briefs for the apps in its repo | See open reports, or anything from other repos |
| Hand a brief to another repo connected to the same app, with what it found | Accept or decline. People do that. |
| Mark a brief fixed, with a note for the reporter | Keep working after you revoke its token (Connected repos in the dashboard) |
Report text is fenced and labelled as a user's description, never instructions, in everything an agent reads, so a report can't take over your agent.
One repo, or several
Each repo gets its own token and sees only its apps. A monorepo links several apps to one token. If one app's code lives in two repos (say a web front end and an API), connect both: when you accept a report, the dashboard asks which repo the fix goes in. An agent that finds the fix belongs elsewhere hands it off, and a claimed brief shows as "In progress in …" so nobody does it twice. Claims lapse after a day without activity.
Without MCP
The same actions are commands: npx heresay briefs, brief <id>, claim <id>, note, handoff, fixed <id> "note", and guide <topic>. Agents can also read /llms.txt and the guides under /guides/ on your Heresay.
Keeping it current
The skill in your repo is small and rarely changes. The detailed steps come from your own Heresay each time, so they match what you have deployed. When a newer skill ships, the agent is told to run npx heresay connect --update, and the change shows up in git.
You can still copy a single prompt from any accepted report in the dashboard with Copy prompt for agent.
Security and limits
- Your key is public, and that's fine. It's in your page's HTML. It only identifies your project; it can't read or change anything.
- Your key only works on your site. Reports from any other website are refused. Leave the list empty when adding an app to allow any site (useful for testing). Your Heresay's own address is always allowed, so its test page works.
- Rate limits. Per device: 5 reports per 10 minutes and 20 per day. Per network: 30 per hour. Per app: 500 per day.
- Size. Reports are up to 2,000 characters. Context fields are trimmed to 200.
Privacy
Heresay stores what the user typed, the type they chose, the page or screen, your app version, browser and OS, and the user id or label only if you pass one. It sets no cookies and takes no screenshots. A random device id lives in the browser's local storage so people can see their own reports.
Content Security Policy
If your site sends a Content Security Policy, allow Heresay in two places:
script-src https://<your-heresay>.web.app;
connect-src https://<your-heresay>.web.app;
iOS and macOS
A Swift package for SwiftUI apps (iOS 16, macOS 13 and later). In Xcode, File › Add Package Dependencies… with https://github.com/Sibhimanyu/heresay-swift, then:
import Heresay
@main struct MyApp: App {
init() { Heresay.configure(key: "pk_your_key", url: URL(string: "https://<your-heresay>.web.app")!) }
var body: some Scene {
WindowGroup { ContentView().heresayReportButton() } // iOS: a Report button in the corner
}
}
On macOS, use ContentView().heresay() and add .commands { HeresayCommands() } for Help › Report a Problem… (⌥⌘R). The same calls as the web SDK exist: Heresay.identify(id:label:email:), Heresay.presentPreferences(), Heresay.introduce() (once per install, a welcome sheet on iOS or a small window on macOS saying where to find it; call it when the main screen appears), , Heresay.setScreen(_:), Heresay.present(type:text:), Heresay.onSent, and Heresay.send(_:text:) for your own UI. How it looks is one HeresayStyle passed to configure(…, style:), with the same choices as the web (corner, offset, text, pill or icon, size, fill, shadow, screens to hide it on, theme, typeface, English, French, Tamil or Hindi, report types, a thank-you line, sheet size, the Preferences tab). The defaults are recommended; the dashboard's Design page shows each choice and gives the Swift. The app version comes from the bundle. Native apps have no web origin to lock the key to; the rate limits still apply.
Other platforms
Android, React Native and Flutter SDKs are on the way. Until then, any app can talk to the same API. Both calls take a JSON body.
POST https://<your-heresay>.web.app/v1/reports
{ "key": "pk_…", "device_id": "a-random-id-16-to-64-chars",
"type": "confusing", "text": "I can't find where to cancel my plan.",
"context": { "route": "Settings", "app_version": "1.4.0", "platform": "ios", "os": "iOS 18" },
"reporter": { "name": "Asha", "email": "asha@example.org", "note": "I use VoiceOver" } } // optional
POST https://<your-heresay>.web.app/v1/reports/mine
{ "key": "pk_…", "device_id": "the-same-id" } // this device's reports and their statuses
Troubleshooting
| You see | Why, and the fix |
|---|---|
| No Report button | Check the script tag has data-key, and look for a CSP error in the browser console. |
| "This site is not allowed to use this key" | The page's address doesn't match the website on your project. Use the exact origin, including https:// and any port. |
| "Too many reports, try again later" | A rate limit was hit. It resets on its own; the response says when. |
| Reports show no version | Add data-version or call Heresay.setVersion(). |