How it works
Three steps, no code to write
- Create your inbox Name your app at the top of the page. No sign-up: you get the key and the dashboard right away. Create now
- Paste the instruction into your AI agent In Lovable, Bolt, v0, Cursor or Replit. It adds the “Was this helpful?” question to your app’s screens.
- See what people say Every vote and comment lands in the dashboard, split by screen. You see which screen confuses people and know what to ask your AI to change.
For when you…
Try it
See how it looks in your app
Find out whether each screen is helping the people who use what you built, in any kind of application: website, web app, iOS, Android, hybrid or native, even bots and servers. People vote with one tap and, if they want, leave a comment and their WhatsApp so you can reply. Everything lands in the same dashboard.
- Web
- iOS
- Android
- Hybrid
- Native
- React Native
- Flutter
- Bots and servers
Using Claude, ChatGPT, Cursor, Lovable, Bolt or v0? Copy the instructions, paste them into your agent's chat and it works out whether the project is web, iOS, Android or something else, and installs it the right way. In your dashboard, the button copies the instructions with your key already in them.
Try it here. Each example shows the code behind the widget: texts, icons, colors, fields and layout change with attributes or JavaScript alone.
Default
<div data-tbbc-rating data-section="demo-padrao"
data-theme="dark"></div>Emojis, color and your own texts
<div data-tbbc-rating data-section="demo-emoji"
data-theme="dark"
data-question="Did you like this page?"
data-up-icon="😍" data-up-label=""
data-down-icon="😕" data-down-label=""
data-accent="#16a34a"
data-title-up="Great! What did you enjoy most?"
data-thanks="Thanks a lot! 💚"></div>Yes / No, stacked, comment only
<div data-tbbc-rating data-section="demo-sim-nao"
data-theme="dark"
data-layout="stacked"
data-question="Did you find what you were looking for?"
data-up-icon="none" data-up-label="Yes"
data-down-icon="none" data-down-label="No"
data-fields="comment"
data-accent="#7c3aed"
style="--tbbc-rating-radius:8px"></div>In Portuguese, light theme
<div data-tbbc-rating data-section="demo-english"
data-lang="pt-BR" data-theme="light"
data-accent="#0f172a"></div>With JavaScript, SVG icon
<div id="avaliacao"></div>
<script>
TBBCRating.mount(document.getElementById('avaliacao'), {
section: 'demo-js',
question: 'Was it worth it?',
upIconHtml: '<svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M12 21s-7-4.4-9.5-8.6C.8 9.3 2.6 5 6.6 5c2.1 0 3.6 1.2 5.4 3 1.8-1.8 3.3-3 5.4-3 4 0 5.8 4.3 4.1 7.4C19 16.6 12 21 12 21z"/></svg>',
upLabel: 'Love it',
downIcon: '💤', downLabel: 'Meh',
accent: '#e11d48',
fields: 'comment,contact',
theme: 'dark',
})
</script>For developers
Manual install, API and options
Script, attributes, API for iOS and Android, and code samples. If you use an AI agent, the instruction from step 2 already covers all of this.
Instalar
Two ways in, one dashboard
Create your site key at tbbc.app/feedbacks and choose by where your interface runs:
Web and hybrid
Widget in two steps
-
Load the script once, on every page
Before
</body>, or in your site's shared layout. The keydata-siteis required: create yours at tbbc.app/feedbacks (it's free). It sends the answers to your team's dashboard, and without it the widget does not show. The script is ready to copy there.<script src="https://tbbc.app/rating-widget/rating.js" data-site="SUA_CHAVE" data-app="meu-app" async></script> -
Put the rating wherever you want
One block per screen or section. The
data-sectionsays which part of the app the vote came from.<div data-tbbc-rating data-section="checkout"></div>Prefer a fixed bar in the corner of every page? Skip this step and use
data-floatingon the script:<script src="https://tbbc.app/rating-widget/rating.js" data-site="SUA_CHAVE" data-app="meu-app" data-floating async></script>
In your framework or site builder
Any site where you can edit the HTML: the two steps above are all it takes. A complete page:
<body>
<h1>Resultado da busca</h1>
...
<div data-tbbc-rating data-section="search"></div>
<script src="https://tbbc.app/rating-widget/rating.js" data-site="SUA_CHAVE" data-app="meu-app" async></script>
</body>Create this component once and use it on any screen: <TbbcRating section="checkout" />. It loads the script only the first time.
'use client' // Next.js App Router. Remove it elsewhere.
import { useEffect, useRef } from 'react'
const SRC = 'https://tbbc.app/rating-widget/rating.js'
export function TbbcRating({ section, site = 'SUA_CHAVE', app = 'meu-app' }) {
const ref = useRef(null)
useEffect(() => {
const mount = () => window.TBBCRating && window.TBBCRating.mount(ref.current)
if (window.TBBCRating) return mount()
let script = document.querySelector(`script[src="${SRC}"]`)
if (!script) {
script = document.createElement('script')
script.src = SRC
script.async = true
script.dataset.site = site
script.dataset.app = app
document.head.appendChild(script)
}
script.addEventListener('load', mount)
return () => script.removeEventListener('load', mount)
}, [site, app])
return <div ref={ref} data-tbbc-rating data-section={section} />
}With TypeScript, declare TBBCRating?: { mount(el: Element | null): void; scan(): void } on Window.
Save it as TbbcRating.vue and use <TbbcRating section="checkout" />. In Nuxt, wrap it in <ClientOnly>.
<script setup>
import { onMounted, ref } from 'vue'
const props = defineProps({ section: String, site: { type: String, default: 'SUA_CHAVE' }, app: { type: String, default: 'meu-app' } })
const el = ref(null)
const SRC = 'https://tbbc.app/rating-widget/rating.js'
onMounted(() => {
const mount = () => window.TBBCRating && window.TBBCRating.mount(el.value)
if (window.TBBCRating) return mount()
let script = document.querySelector(`script[src="${SRC}"]`)
if (!script) {
script = document.createElement('script')
script.src = SRC
script.async = true
script.dataset.site = props.site
script.dataset.app = props.app
document.head.appendChild(script)
}
script.addEventListener('load', mount)
})
</script>
<template>
<div ref="el" data-tbbc-rating :data-section="section"></div>
</template>- Script: paste the code from step 1 into your site's footer code field. In WordPress, use a script-insertion plugin (such as “WPCode”) or the theme footer. In Webflow, Wix and Framer, look for “Custom code” in the site settings.
- Rating: on the page, add a “Custom HTML” (or “Embed”) block with the code from step 2.
- Publish the site. The rating shows on the published version; some editors do not run scripts in preview.
With an AI agent
Let your agent install it
The instructions tell the agent what the rating is, how to identify the kind of project, where to put it in each framework, how to build the native interface on iOS and Android, what to do with each API response and how to test it. It asks which screens you want the rating on, if you don't say.
Agents that read links load the right guide on their own from the index https://tbbc.app/rating-widget/llms.txt. The guides: web and iOS, Android, native and API.
See the full instructions
Loading…
iOS, Android, native, bots and servers
Use the API
iOS or Android app, native or built with React Native and Flutter, desktop software, WhatsApp bot, kiosk, post-purchase e-mail: any system that can make an HTTP call sends ratings to the same dashboard. You use the same site key and build the question with your app's components. A suggested interface, the same as the widget: “Was this helpful?” with +1 and −1; after the vote, an optional form with comment, name and WhatsApp or e-mail.
The flow
- The person votes +1 or −1. You send the vote and get an
id. - Open the “what did you think?” question right away, without waiting for the vote's response. Send the comment, name and contact with that
idwithin 24 hours. You can send more than once: each comment is added to the vote. - The vote and the comment show up in your team's dashboard, marked “via API”.
Base URL: https://tbbc.app. Send JSON, with the header Content-Type: application/json. There is no other login: the key says which site and team the rating goes to.
Records a vote.
| Field | Type | What it is |
|---|---|---|
site | text, required | Your key, created at tbbc.app/feedbacks. Starts with k_. |
vote | number, required | 1 (liked it) or -1 (did not like it). |
section | text | Part of the system: pos-venda, app/checkout, bot/suporte. Letters, numbers and - _ . /. Without it: default. |
app | text | Name of the system, to tell projects with the same key apart. Up to 80 characters. |
page | text | The screen or step, as a path: /pedido/entregue. Parameters after ? are dropped. |
lang | text | The person's language, such as pt-BR. |
origin | URL | Web systems only. Without it, the vote is marked “via API”. |
curl -X POST https://tbbc.app/api/rating \
-H "Content-Type: application/json" \
-d '{"site": "SUA_CHAVE", "vote": -1, "section": "pos-venda", "app": "meu-bot"}'Success response (201):
{ "ok": true, "id": "3f0c9a52-8a3e-4c4f-9a43-1c2b7d1e5a10" }What to do with the response
| Response | What it means | What to do |
|---|---|---|
201 | Vote saved. | Keep the id for up to 24 hours if you will ask for a comment. Otherwise you can drop it. |
400 site_key_is_required | Missing site. | A bug in your code: send the key. Do not repeat the call without fixing it. |
400 unknown_site_key | The key does not exist. | Check the key in the dashboard. Nothing was saved. |
400 vote_must_be_1_or_-1 | vote not 1 or -1. | Fix the value. Send a number, not text. |
500 or no response | A failure on our side or in the network. | Do not show an error to the person. Keep the vote in a local queue and try again, waiting longer each time (up to 1 minute), and right away when the network comes back. If the response was lost after saving, the retry may create a duplicate vote. |
Adds the comment, name and contact to the vote. Send at least one of the three. You can call it more than once within 24 hours: each comment is added to the previous ones, and name and contact are updated.
| Field | Type | What it is |
|---|---|---|
comment | text | What the person said. Up to 1,000 characters. |
name | text | Their name, if they want. Up to 80 characters. |
contact | text | WhatsApp with area code (10 to 15 digits, with or without +55) or e-mail. Shows in the dashboard as a link to reply. |
nonce | text | A unique id per submission (a UUID, for example). Repeating the call with the same nonce does not duplicate the comment. Always send it. |
curl -X POST https://tbbc.app/api/rating/3f0c9a52-8a3e-4c4f-9a43-1c2b7d1e5a10 \
-H "Content-Type: application/json" \
-d '{"comment": "A entrega atrasou dois dias", "name": "Rui", "contact": "+55 44 98818-0252", "nonce": "b7f1c2"}'Success response (200):
{ "ok": true, "id": "3f0c9a52-8a3e-4c4f-9a43-1c2b7d1e5a10" }What to do with the response
| Response | What it means | What to do |
|---|---|---|
200 | Comment saved on the vote. | Show the thank-you (ideally right when they submit) and offer “Anything else you want to say?”, using the same id. |
400 contact_is_invalid | The contact is neither a valid WhatsApp number nor a valid e-mail. | Ask the person to check the number or e-mail, or send without the contact. |
400 comment_too_long / name_too_long | Over the limit. | Limit the fields to 1,000 and 80 characters. Together, a vote's comments go up to 5,000. |
400 nothing_to_save | All three fields were empty. | Do not call this endpoint when the person wrote nothing. |
404 not_found | This id does not exist. | Use the exact id the vote returned. |
409 too_many_follow_ups | The vote already got 20 submissions. | Drop it: retrying will not help. |
409 too_late | More than 24 hours since the vote. | If you still want to record it, send a new vote and then the comment. |
500 or no response | A failure on our side or in the network. | Do not show an error. Keep it in the queue and retry with the same nonce: if it was already saved, the response is 200 and nothing is duplicated. |
Examples
React Native, Expo, Node 18+, Deno or Bun. For Flutter, the agent guide has the Dart example.
const TBBC = 'https://tbbc.app/api/rating'
const SITE = 'SUA_CHAVE'
async function post(url, body) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json().catch(() => ({}))
return { status: res.status, ...data }
}
// 1. The vote: keep the id if you will ask for a comment
export async function vote(value, section) {
const r = await post(TBBC, { site: SITE, vote: value, section, app: 'meu-app' })
if (r.status === 201) return r.id
throw new Error(r.error || `HTTP ${r.status}`) // 400: fix the code; 500: try again
}
// 2. The comment (optional, can repeat, within 24 h).
// When retrying, send the SAME nonce: the server ignores the repeat.
export async function comment(id, { comment, name, contact, nonce = Date.now().toString(36) + Math.random().toString(36).slice(2) }) {
const r = await post(`${TBBC}/${id}`, { comment, name, contact, nonce })
if (r.status === 200) return true
if (r.error === 'contact_is_invalid') return 'ask them to check the contact'
throw new Error(r.error || `HTTP ${r.status}`)
}iOS and macOS, with URLSession, no libraries:
import Foundation
enum TbbcRating {
static let base = URL(string: "https://tbbc.app/api/rating")!
static let site = "SUA_CHAVE"
struct Reply: Decodable { let ok: Bool?; let id: String?; let error: String? }
private static func post(_ url: URL, _ body: [String: Any]) async throws -> (Int, Reply) {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
return (status, (try? JSONDecoder().decode(Reply.self, from: data)) ?? Reply(ok: nil, id: nil, error: nil))
}
/// Returns the rating id (keep it to send the comment).
static func vote(_ value: Int, section: String) async throws -> String {
let (status, reply) = try await post(base, ["site": site, "vote": value, "section": section, "app": "meu-app-ios"])
guard status == 201, let id = reply.id else { throw URLError(.badServerResponse) }
return id
}
/// Appends a comment to the vote. Retry with the SAME nonce: the server ignores repeats.
/// Throws on errors; "contact_is_invalid" asks the person to fix it.
static func comment(id: String, nonce: String = UUID().uuidString, comment: String?, name: String?, contact: String?) async throws -> Bool {
var body: [String: Any] = ["comment": comment, "name": name, "contact": contact].compactMapValues { $0 }
body["nonce"] = nonce
let (status, reply) = try await post(base.appendingPathComponent(id), body)
if status == 200 { return true }
throw NSError(domain: "TbbcRating", code: status, userInfo: [NSLocalizedDescriptionKey: reply.error ?? "HTTP \(status)"])
}
}Android, no extra libraries. Call it off the main thread and make sure you have the permission INTERNET:
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
object TbbcRating {
private const val BASE = "https://tbbc.app/api/rating"
private const val SITE = "SUA_CHAVE"
private fun post(url: String, body: JSONObject): Pair<Int, JSONObject> {
val conn = URL(url).openConnection() as HttpURLConnection
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", "application/json")
conn.connectTimeout = 10_000
conn.readTimeout = 10_000
conn.doOutput = true
conn.outputStream.use { it.write(body.toString().toByteArray()) }
val status = conn.responseCode
val text = (if (status < 400) conn.inputStream else conn.errorStream)?.bufferedReader()?.readText().orEmpty()
conn.disconnect()
return status to (runCatching { JSONObject(text) }.getOrDefault(JSONObject()))
}
/** Returns the rating id (keep it to send the comment). */
fun vote(value: Int, section: String): String {
val (status, reply) = post(BASE, JSONObject().put("site", SITE).put("vote", value).put("section", section).put("app", "meu-app-android"))
check(status == 201) { reply.optString("error", "HTTP $status") }
return reply.getString("id")
}
/** Appends a comment to the vote. Retry with the SAME nonce: the server ignores repeats.
* Throws with the error code otherwise ("contact_is_invalid": ask to fix). */
fun comment(id: String, comment: String?, name: String?, contact: String?, nonce: String = java.util.UUID.randomUUID().toString()): Boolean {
val body = JSONObject().put("nonce", nonce)
comment?.let { body.put("comment", it) }
name?.let { body.put("name", it) }
contact?.let { body.put("contact", it) }
val (status, reply) = post("$BASE/$id", body)
if (status == 200) return true
error(reply.optString("error", "HTTP $status"))
}
}import uuid
import requests
TBBC = "https://tbbc.app/api/rating"
SITE = "SUA_CHAVE"
def vote(value, section):
r = requests.post(TBBC, json={"site": SITE, "vote": value, "section": section, "app": "meu-sistema"}, timeout=10)
if r.status_code == 201:
return r.json()["id"] # keep it if you will ask for a comment
r.raise_for_status() # 400: fix the code; 500: try again
def comment(rating_id, comment=None, name=None, contact=None, nonce=None):
# When retrying, send the SAME nonce: the server ignores the repeat.
body = {"comment": comment, "name": name, "contact": contact, "nonce": nonce or uuid.uuid4().hex}
r = requests.post(f"{TBBC}/{rating_id}", json=body, timeout=10)
if r.status_code == 200:
return True
if r.json().get("error") == "contact_is_invalid":
return False # ask them to check the contact
r.raise_for_status()A simple way to ask at the end of a conversation:
- The bot asks: “Did this help you? Reply 1 for yes or 2 for no.”
- On the reply, send
POST /api/ratingwithvote: 1or-1andsection: "bot/atendimento". Keep theidwith the conversation. - The bot asks: “Want to tell us more? Just write. If not, reply 0.”
- If the person writes, send
POST /api/rating/{id}with the text incommentand their number incontact, which you already have. Reply “Got it, thank you! If you want to say anything else, just write.” and add each new message to the same vote. If they reply 0, call nothing.
Let the person know that the answer, with their number, goes to your team.
Good to know
- The key is public: it only allows sending ratings to your site. Reading the answers takes your team's login in the dashboard.
- Send only what the person chose to share. Do not put e-mails, phone numbers or ids in
section,apporpage. - Do not send the field
website: it is a trap for bots, and the call is refused when it is filled.
Opções
Customize everything
Texts, icons, colors, fields and layout change in code. Put the options on the <script> (they apply to every widget on the page), on a data-tbbc-rating block (just that one), or in JavaScript. The examples in “Try it here” show the result.
Installation
| Attribute | Where | What it does |
|---|---|---|
data-site | script | Required. The app key, created at tbbc.app/feedbacks. It sends the answers to your team's dashboard. |
data-app | script | Name of the system, to tell projects with the same key apart. |
data-section | block | Part of the app: letters, numbers and - _ . /. Without it: default. |
data-floating | script | A fixed bar in the bottom-right corner of every page. |
Texts
Any text can be changed, on the block or the script. To remove a button's label and keep only the icon, leave it empty: data-up-label="".
| Attribute | Default |
|---|---|
data-question | Was this helpful? |
data-up-label / data-down-label | +1 / −1 |
data-title-up / data-title-down | Great! What did you like? / What can we improve? |
data-placeholder | A few words (optional) |
data-name-label / data-contact-label | Your name (optional) / WhatsApp or e-mail (optional) |
data-contact-hint | Only to get back to you, if needed. |
data-send / data-skip | Send / Not now |
data-thanks / data-more | Got it, thank you! / Anything else you want to say? |
data-voted / data-leave-comment | Thanks for voting! / Leave a comment |
data-lang | Follows the page language. pt-BR or en switches all default texts. |
Icons, colors, fields and layout
| Attribute | What it does |
|---|---|
data-up-icon / data-down-icon | An emoji or text instead of the thumb, such as 😍. none removes the icon. For SVG, use JavaScript (below). |
data-accent | Color of the chosen button, the Send button and links. Any CSS color. |
data-theme | light or dark. Without it, follows the system. |
data-layout | inline (default, one line) or stacked (question on top, buttons below). |
data-fields | Form fields, comma-separated: comment,name,contact (default), any subset, or none to vote only. |
More tweaks with CSS variables on the block:
[data-tbbc-rating] {
--tbbc-rating-accent: #16a34a; /* highlight */
--tbbc-rating-accent-text: #ffffff; /* text on the highlight */
--tbbc-rating-bg: #ffffff; /* background */
--tbbc-rating-text: #111111; /* text */
--tbbc-rating-muted: #6b7280; /* secondary text */
--tbbc-rating-border: #e5e7eb; /* borders */
--tbbc-rating-radius: 8px; /* corners (default: pill) */
--tbbc-rating-font: Inter, sans-serif;
--tbbc-rating-font-size: 15px;
}In JavaScript
The same options, with camelCase names. TBBCRating.configure() applies to every widget mounted afterwards; TBBCRating.mount() mounts one element with its own options. Only JavaScript can use HTML or SVG icons (upIconHtml, downIconHtml).
TBBCRating.configure({ accent: '#16a34a', thanks: 'Thanks!' })
TBBCRating.mount(document.getElementById('avaliacao'), {
section: 'checkout',
question: 'Did the payment go well?',
upIconHtml: '<svg ...></svg>',
upLabel: 'Yes',
downLabel: 'No',
fields: 'comment',
})Single-page apps
If the screen is rendered after load (route changes without a reload), call window.TBBCRating.scan() after the block appears. The React and Vue components above already do it.
Content-Security-Policy
If your site has a CSP, allow script-src https://tbbc.app and connect-src https://tbbc.app.
Data
What is stored
- The vote (+1 or −1), the site's domain, the section and the page path, without URL parameters.
- Comment, name and WhatsApp or e-mail only when the person fills them in.
- No cookies and no cross-site tracking.
If your site has a privacy policy, mention that submitted ratings are stored by TBBC.
Answers
See what people said
In the dashboard you see how many ratings each section got, the share of positive ones and every comment. Mark what you already solved, give it a category and note what you did. Each site belongs to a team: every member sees and organizes the same feedback.