Feedback para qualquer app
Avaliação +1 / −1 para qualquer app: web, iOS, Android
Saiba se cada tela está ajudando quem usa o que você construiu, em qualquer tipo de aplicação: site, app web, iOS, Android, híbrido ou nativo, e até bots e servidores. A pessoa vota com um toque e, se quiser, deixa um comentário e o WhatsApp para você responder. Tudo chega no mesmo painel.
- Web
- iOS
- Android
- Híbrido
- Nativo
- React Native
- Flutter
- Bots e servidores
Usa Claude, ChatGPT, Cursor, Lovable, Bolt ou v0? Copie as instruções, cole no chat do seu agente e ele descobre sozinho se o projeto é web, iOS, Android ou outro, e instala do jeito certo. No seu painel, o botão já copia as instruções com a sua chave.
Experimente aqui. Cada exemplo mostra o código que gera o widget: textos, ícones, cores, campos e layout mudam só com atributos ou JavaScript.
Padrão
<div data-tbbc-rating data-section="demo-padrao"
data-theme="dark"></div>Emojis, cor e textos próprios
<div data-tbbc-rating data-section="demo-emoji"
data-theme="dark"
data-question="Gostou desta página?"
data-up-icon="😍" data-up-label=""
data-down-icon="😕" data-down-label=""
data-accent="#16a34a"
data-title-up="Que bom! O que mais você curtiu?"
data-thanks="Valeu demais! 💚"></div>Sim / Não, empilhado, só comentário
<div data-tbbc-rating data-section="demo-sim-nao"
data-theme="dark"
data-layout="stacked"
data-question="Encontrou o que procurava?"
data-up-icon="none" data-up-label="Sim"
data-down-icon="none" data-down-label="Não"
data-fields="comment"
data-accent="#7c3aed"
style="--tbbc-rating-radius:8px"></div>Em inglês, tema claro
<div data-tbbc-rating data-section="demo-english"
data-lang="en" data-theme="light"
data-accent="#0f172a"></div>Por JavaScript, com ícone SVG
<div id="avaliacao"></div>
<script>
TBBCRating.mount(document.getElementById('avaliacao'), {
section: 'demo-js',
question: 'Valeu a pena?',
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: 'Amei',
downIcon: '💤', downLabel: 'Meh',
accent: '#e11d48',
fields: 'comment,contact',
theme: 'dark',
})
</script>Instalar
Dois caminhos, o mesmo painel
Crie a chave do seu site em tbbc.app/feedbacks e escolha pelo lugar onde a sua interface roda:
Web e híbrido
Widget em dois passos
-
Carregue o script uma vez, em todas as páginas
Antes de
</body>, ou no layout comum do site. A chavedata-siteé obrigatória: crie a sua em tbbc.app/feedbacks (é grátis). Ela leva as respostas para o painel do seu time, e sem ela o widget não aparece. Lá o script já vem pronto para copiar.<script src="https://tbbc.app/rating-widget/rating.js" data-site="SUA_CHAVE" data-app="meu-app" async></script> -
Coloque a avaliação onde quiser
Um bloco por tela ou seção. O
data-sectiondiz de qual parte do app veio o voto.<div data-tbbc-rating data-section="checkout"></div>Prefere uma barra fixa no canto de todas as páginas? Pule este passo e use
data-floatingno script:<script src="https://tbbc.app/rating-widget/rating.js" data-site="SUA_CHAVE" data-app="meu-app" data-floating async></script>
No seu framework ou construtor
Qualquer site em que você mexe no HTML: os dois passos acima são tudo. Exemplo de página completa:
<body>
<h1>Resultado da busca</h1>
...
<div data-tbbc-rating data-section="busca"></div>
<script src="https://tbbc.app/rating-widget/rating.js" data-site="SUA_CHAVE" data-app="meu-app" async></script>
</body>Crie este componente uma vez e use em qualquer tela: <TbbcRating section="checkout" />. Ele carrega o script só na primeira vez.
'use client' // Next.js App Router. Remova nos outros casos.
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} />
}Com TypeScript, declare TBBCRating?: { mount(el: Element | null): void; scan(): void } em Window.
Salve como TbbcRating.vue e use <TbbcRating section="checkout" />. No Nuxt, coloque dentro de <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: cole o código do passo 1 no campo de código do rodapé do site. No WordPress, use um plugin de inserir scripts (como “WPCode”) ou o rodapé do tema. No Webflow, Wix e Framer, procure “Custom code” ou “Código personalizado” nas configurações do site.
- Avaliação: na página, adicione um bloco de “HTML personalizado” (ou “Embed”) com o código do passo 2.
- Publique o site. A avaliação aparece na versão publicada; alguns editores não rodam scripts na pré-visualização.
Com um agente de IA
Deixe o seu agente instalar
As instruções explicam ao agente o que é a avaliação, como identificar o tipo do projeto, onde colocar em cada framework, como montar a interface nativa no iOS e no Android, o que fazer com cada resposta da API e como testar. Ele pergunta em quais telas você quer a avaliação, se você não disser.
Agentes que leem links carregam o guia certo sozinhos a partir do índice https://tbbc.app/rating-widget/llms.txt. Os guias: web e iOS, Android, nativo e API.
Ver as instruções completas
Carregando…
iOS, Android, nativo, bots e servidores
Use a API
App iOS ou Android, nativo ou em React Native e Flutter, sistema desktop, bot de WhatsApp, totem, e-mail pós-venda: qualquer sistema que faz uma chamada HTTP manda avaliações para o mesmo painel. Você usa a mesma chave do site e monta a pergunta com os componentes do seu app. Sugestão de interface, igual à do widget: “Esta tela foi útil?” com +1 e −1; depois do voto, um formulário opcional com comentário, nome e WhatsApp ou e-mail.
O fluxo
- A pessoa vota +1 ou −1. Você envia o voto e recebe um
id. - Abra a pergunta “o que você achou?” na hora, sem esperar a resposta do voto. Envie o comentário, o nome e o contato com esse
idem até 24 horas. Pode enviar mais de uma vez: cada comentário é acrescentado ao voto. - O voto e o comentário aparecem no painel do seu time, marcados como “via API”.
Endereço base: https://tbbc.app. Envie JSON, com o header Content-Type: application/json. Não há outro login: a chave diz para qual site e time vai a avaliação.
Registra um voto.
| Campo | Tipo | O que é |
|---|---|---|
site | texto, obrigatório | Sua chave, criada em tbbc.app/feedbacks. Começa com k_. |
vote | número, obrigatório | 1 (gostou) ou -1 (não gostou). |
section | texto | Parte do sistema: pos-venda, app/checkout, bot/suporte. Letras, números e - _ . /. Sem ele: default. |
app | texto | Nome do sistema, para separar projetos com a mesma chave. Até 80 caracteres. |
page | texto | A tela ou o passo, como um caminho: /pedido/entregue. Parâmetros depois de ? são descartados. |
lang | texto | Idioma da pessoa, como pt-BR. |
origin | URL | Só para sistemas web. Sem ela, o voto fica marcado como “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"}'Resposta de sucesso (201):
{ "ok": true, "id": "3f0c9a52-8a3e-4c4f-9a43-1c2b7d1e5a10" }O que fazer com a resposta
| Resposta | O que significa | O que fazer |
|---|---|---|
201 | Voto gravado. | Guarde o id por até 24 horas, se for pedir um comentário. Senão, pode descartar. |
400 site_key_is_required | Faltou o site. | Erro no seu código: envie a chave. Não repita a chamada sem corrigir. |
400 unknown_site_key | A chave não existe. | Confira a chave no painel. Nada foi gravado. |
400 vote_must_be_1_or_-1 | vote fora de 1 ou -1. | Corrija o valor. Envie número, não texto. |
500 ou sem resposta | Falha do nosso lado ou da rede. | Não mostre erro para a pessoa. Guarde o voto numa fila local e tente de novo, esperando cada vez mais (até 1 minuto) e na hora quando a rede voltar. Se a resposta se perdeu depois de gravar, a nova tentativa pode criar um voto duplicado. |
Acrescenta o comentário, o nome e o contato ao voto. Envie pelo menos um dos três. Pode chamar mais de uma vez em até 24 horas: cada comentário é somado aos anteriores, e nome e contato são atualizados.
| Campo | Tipo | O que é |
|---|---|---|
comment | texto | O que a pessoa disse. Até 1.000 caracteres. |
name | texto | Nome, se ela quiser. Até 80 caracteres. |
contact | texto | WhatsApp com DDD (10 a 15 dígitos, com ou sem +55) ou e-mail. Aparece no painel como link para responder. |
nonce | texto | Um id único por envio (um UUID, por exemplo). Repetir a chamada com o mesmo nonce não duplica o comentário. Use sempre. |
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"}'Resposta de sucesso (200):
{ "ok": true, "id": "3f0c9a52-8a3e-4c4f-9a43-1c2b7d1e5a10" }O que fazer com a resposta
| Resposta | O que significa | O que fazer |
|---|---|---|
200 | Comentário gravado no voto. | Mostre o obrigado (de preferência já na hora do envio) e ofereça “Quer dizer mais alguma coisa?”, usando o mesmo id. |
400 contact_is_invalid | O contato não é um WhatsApp nem um e-mail válido. | Peça para a pessoa conferir o número ou o e-mail, ou envie sem o contato. |
400 comment_too_long / name_too_long | Passou do limite. | Limite os campos a 1.000 e 80 caracteres. Somados, os comentários de um voto vão até 5.000. |
400 nothing_to_save | Os três campos vieram vazios. | Não chame este endpoint quando a pessoa não escreveu nada. |
404 not_found | Esse id não existe. | Use o id exato que o voto devolveu. |
409 too_many_follow_ups | O voto já recebeu 20 envios. | Descarte: repetir não adianta. |
409 too_late | Passaram mais de 24 horas desde o voto. | Se ainda quiser registrar, envie um voto novo e o comentário em seguida. |
500 ou sem resposta | Falha do nosso lado ou da rede. | Não mostre erro. Mantenha na fila e tente de novo com o mesmo nonce: se já tiver gravado, a resposta é 200 e nada é duplicado. |
Exemplos
React Native, Expo, Node 18+, Deno ou Bun. No Flutter, o guia para agentes tem o exemplo em Dart.
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. O voto: guarde o id se for pedir comentário
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: corrija o código; 500: tente de novo
}
// 2. O comentário (opcional, pode repetir, em até 24 h).
// Ao tentar de novo, mande o MESMO nonce: o servidor ignora a repetição.
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 'peça para conferir o contato'
throw new Error(r.error || `HTTP ${r.status}`)
}iOS e macOS, com URLSession, sem bibliotecas:
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, sem bibliotecas extras. Chame fora da thread principal e garanta a permissão 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"] # guarde se for pedir comentário
r.raise_for_status() # 400: corrija o código; 500: tente de novo
def comment(rating_id, comment=None, name=None, contact=None, nonce=None):
# Ao tentar de novo, mande o MESMO nonce: o servidor ignora a repetição.
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 # peça para conferir o contato
r.raise_for_status()Um jeito simples de perguntar no fim de um atendimento:
- O bot pergunta: “Este atendimento te ajudou? Responda 1 para sim ou 2 para não.”
- Na resposta, envie
POST /api/ratingcomvote: 1ou-1esection: "bot/atendimento". Guarde oidjunto da conversa. - O bot pergunta: “Quer contar mais? É só escrever. Se não, responda 0.”
- Se a pessoa escrever, envie
POST /api/rating/{id}com o texto emcommente o número dela emcontact, que você já tem. Responda “Recebido, obrigado! Se quiser dizer mais alguma coisa, é só escrever.” e acrescente cada nova mensagem ao mesmo voto. Se responder 0, não chame nada.
Avise a pessoa de que a resposta, com o número dela, vai para a sua equipe.
Bom saber
- A chave é pública: ela só permite enviar avaliações para o seu site. Ler as respostas exige o login do seu time no painel.
- Envie só o que a pessoa escolheu informar. Não coloque e-mail, telefone ou ids em
section,appoupage. - Não envie o campo
website: ele é uma armadilha para robôs, e a chamada é recusada quando ele vem preenchido.
Opções
Customize tudo
Textos, ícones, cores, campos e layout mudam por código. Coloque as opções no <script> (valem para todos os widgets da página), num bloco data-tbbc-rating (só aquele), ou em JavaScript. Os exemplos em “Experimente aqui” mostram o resultado.
Instalação
| Atributo | Onde | O que faz |
|---|---|---|
data-site | script | Obrigatório. A chave do app, criada em tbbc.app/feedbacks. Leva as respostas para o painel do seu time. |
data-app | script | Nome do sistema, para separar projetos com a mesma chave. |
data-section | bloco | Parte do app: letras, números e - _ . /. Sem ele: default. |
data-floating | script | Barra fixa no canto inferior direito, em todas as páginas. |
Textos
Qualquer texto pode ser trocado, no bloco ou no script. Para tirar o rótulo de um botão e deixar só o ícone, use vazio: data-up-label="".
| Atributo | Padrão |
|---|---|
data-question | Esta tela foi útil? |
data-up-label / data-down-label | +1 / −1 |
data-title-up / data-title-down | Que bom! O que você gostou? / O que podemos melhorar? |
data-placeholder | Conte em poucas palavras (opcional) |
data-name-label / data-contact-label | Seu nome (opcional) / WhatsApp ou e-mail (opcional) |
data-contact-hint | Só para responder você, se precisar. |
data-send / data-skip | Enviar / Agora não |
data-thanks / data-more | Recebido, obrigado! / Quer dizer mais alguma coisa? |
data-voted / data-leave-comment | Obrigado pelo voto! / Deixar um comentário |
data-lang | Segue o idioma da página. pt-BR ou en troca todos os textos padrão. |
Ícones, cores, campos e layout
| Atributo | O que faz |
|---|---|
data-up-icon / data-down-icon | Um emoji ou texto no lugar do polegar, como 😍. none tira o ícone. Para SVG, use JavaScript (abaixo). |
data-accent | Cor do botão escolhido, do Enviar e dos links. Qualquer cor CSS. |
data-theme | light ou dark. Sem ele, segue o sistema. |
data-layout | inline (padrão, em uma linha) ou stacked (pergunta em cima, botões embaixo). |
data-fields | Campos do formulário, separados por vírgula: comment,name,contact (padrão), qualquer parte deles, ou none para só votar. |
Mais ajustes com variáveis CSS no bloco:
[data-tbbc-rating] {
--tbbc-rating-accent: #16a34a; /* destaque */
--tbbc-rating-accent-text: #ffffff; /* texto sobre o destaque */
--tbbc-rating-bg: #ffffff; /* fundo */
--tbbc-rating-text: #111111; /* texto */
--tbbc-rating-muted: #6b7280; /* textos secundários */
--tbbc-rating-border: #e5e7eb; /* bordas */
--tbbc-rating-radius: 8px; /* cantos (padrão: pílula) */
--tbbc-rating-font: Inter, sans-serif;
--tbbc-rating-font-size: 15px;
}Em JavaScript
As mesmas opções, com os nomes em camelCase. TBBCRating.configure() vale para todos os widgets montados depois; TBBCRating.mount() monta um elemento com opções próprias. Só pelo JavaScript dá para usar ícones em HTML ou SVG (upIconHtml, downIconHtml).
TBBCRating.configure({ accent: '#16a34a', thanks: 'Valeu!' })
TBBCRating.mount(document.getElementById('avaliacao'), {
section: 'checkout',
question: 'Deu tudo certo com o pagamento?',
upIconHtml: '<svg ...></svg>',
upLabel: 'Sim',
downLabel: 'Não',
fields: 'comment',
})Apps de uma página
Se a tela é montada depois do carregamento (troca de rota sem recarregar), chame window.TBBCRating.scan() depois que o bloco aparecer. Os componentes de React e Vue acima já fazem isso.
Content-Security-Policy
Se o seu site tem CSP, libere script-src https://tbbc.app e connect-src https://tbbc.app.
Dados
O que é guardado
- O voto (+1 ou −1), o domínio do site, a seção e o caminho da página, sem os parâmetros da URL.
- Comentário, nome e WhatsApp ou e-mail só quando a pessoa preenche.
- Nada de cookies nem rastreamento entre sites.
Se o seu site tem política de privacidade, inclua que as avaliações enviadas são guardadas pela TBBC.
Respostas
Veja o que as pessoas disseram
No painel você vê quantas avaliações cada seção recebeu, a porcentagem de positivas e cada comentário. Marque o que já resolveu, dê uma categoria e anote o que fez. Cada site pertence a um time: todos os membros veem e organizam os mesmos feedbacks.