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 |
data-trail | on · off · the parts to keep, like pages errors requests. What happened before a report: pages, clicks, failed requests, errors. The reporter sees it and can take it off. Never what anyone typed. |
data-heresay-private | on any element of your app: clicks on it are recorded without its text |
data-nudge | off · on · the signals to watch, like clicks errors. When something failed (a button pressed again and again with nothing happening, an error right after a click, requests failing), a small note by the button asks “Something not working?”. When someone seems unsure (stalled: typing that stopped; circles: the same few pages over and over; refused: a field refused 3 times), it asks “Stuck on something?” and opens as Confusing. Watched on the device, never sent. Once per tab; quiet for 7 days after “Not now”. |
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.nudge() | Your app saw trouble itself, such as an error screen or a retry that failed: a small note by the button offers to report it. nudge({ stuck: true }) for someone who seems unsure. Works without data-nudge; the same limits apply. Returns whether it showed. |
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 chat starts in a connected repo, Heresay checks for updates and the agent brings them up and asks before applying any: a newer Heresay (whoever set it up runs npx create-heresay update; web apps then get the new Report button with no code change), a newer Swift package than the one pinned in Package.resolved, older agent files (npx heresay connect --update), and features your app doesn't use yet. In Claude Code a session-start hook in .claude/settings.json runs the check, at most once a day per repo; other agents are asked to by AGENTS.md. A no is kept in .claude/skills/heresay/decisions.json, so nobody asks again. You can also ask "Update Heresay", or run npx heresay updates.
You can still copy a single prompt from any accepted report in the dashboard with Copy prompt for agent.
ChatGPT and other chat apps
Ideas that come up in a conversation can go straight to an app's inbox, with what the conversation worked out. Your Heresay is an MCP server at https://<your-heresay>/v1/mcp. In ChatGPT, open Settings › Apps & Connectors, turn on Developer mode under Advanced settings, choose Create, paste that address and pick OAuth. ChatGPT opens your dashboard to ask if it may send requests as you. Then say “Send this to Heresay as an idea for Greenroom”.
Requests arrive as from you (From ChatGPT · From the team) and wait for someone to accept them, like any report. The chat app can list your apps, send requests and say what happened to yours; it can't read anyone else's reports, or accept, decline or fix anything. Chat apps in the dashboard shows each connection and disconnects it. Claude (Settings › Connectors › Add custom connector), Perplexity (Settings › Connectors › Add custom connector › Remote, Streamable HTTP, OAuth 2.0), Gemini (Settings & help › Connected apps › Custom apps, where Google offers it), Cursor and other remote MCP clients use the same address; the dashboard's Chat apps page has the steps for each.
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. Reports carry what happened before them, as on the web: the screens named with .heresayScreen(_:) or setScreen, the app's own Logger errors, the last crash, and on macOS clicks (by accessibility label) and menu commands. The reporter sees it and can take it off; configure(…, trail: []) turns it off. 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(). |