Embed the capture box

The embed widget is one script tag that drops a "Tell us anything" box into your own app. It posts to the same board as your public page, so feedback left in your product and feedback left on the board land in the same feed, in the same order, with the same triage.

It is deliberately small. Everything below is the whole feature.

Get your snippet

Open your product's settings and find Embed the capture box. The snippet is generated for you, already carrying your workspace and product key:

<script
	src="https://api.convo.randomfact.com/widget.js"
	data-board="acme-co/WEB"
	async
></script>

Your product has to be public to get a snippet — a private product tells you so instead. Paste the tag just before the closing </body> tag of your app's HTML shell. The widget appends its own elements to document.body, so a tag in <head> can run before there is a body to append to.

Do not add type="module". The script reads document.currentScript to find its own attributes, and that is always null inside a module.

The attributes

Attribute Required Default What it does
data-board Yes — Your board, as {workspace}/{PRODUCT KEY} — the same pair as your public URL. Anything else logs a console error and the widget does not render.
data-api No The URL the script was loaded from, minus /widget.js — so https://api.convo.randomfact.com Where submissions are posted. You never need this, including against a local Convo; it is an override for proxying the API through your own host.
data-site No https://convo.randomfact.com The base the "See the board" link is built from.
data-accent No #3f7ca5 The launcher, the Send button, and the link. Any CSS color.
data-label No Feedback The launcher's text and its accessible label.

A fuller example:

<script
	src="https://api.convo.randomfact.com/widget.js"
	data-board="acme-co/WEB"
	data-accent="#0d8a68"
	data-label="Tell us anything"
	async
></script>

What it renders

A pill launcher in the bottom-right corner, 20px in from each edge, in your accent color. Clicking it opens a panel above it: 320px wide, or the width of the viewport minus 40px on a narrow screen.

The panel holds, in order:

  • an optional title field, placeholder "Title (optional)", capped at 500 characters;
  • the body textarea, placeholder "Tell us anything…", three rows, which takes focus when the panel opens;
  • a hidden honeypot field, positioned offscreen, that no person ever sees;
  • a See the board link, opening {data-site}/{workspace}/{KEY} in a new tab, and a Send button;
  • a status line.

Sending with an empty body does nothing but refocus the textarea — the note is what is required, the title is a bonus. While the request is in flight the status line reads Sending…. On success it reads Thanks — posted., both fields clear, and the panel closes itself about a second later. On any failure it reads Could not send. Try again? and your text is left in place so nothing is lost.

The whole thing is inline-styled and dependency-free, so it does not read your CSS and your CSS cannot break it.

How identity works

The widget is the anonymous floor of Convo's identity ladder, and it stays there. There is no sign-in, no email field, and nothing for a person to fill in beyond their message.

What it does have is coherence. On first use it mints a random id and keeps it in localStorage under a key named after the board:

convo_fp_acme-co/WEB

Every later post from that browser sends the same id, so all of them attach to one person record in your workspace instead of arriving as unrelated strangers. A capture surface of your own built on the same endpoint can reuse the key and cohere with the widget too. If storage is blocked — private browsing, a strict extension — the widget mints a throwaway id for that one submission and the post still goes through; it just will not cohere with anything else.

Climbing the ladder happens on the board, not in the widget. That is what the "See the board" link is for: someone who wants to be recognised, hear about their request, or take part in the thread goes there and continues with Google or an email address.

The two do meet. A visitor there is identified by an anonymous sign-in session rather than by the widget's key — but the board reads the key, hands it to Convo once, and the server moves everything already posted under that fingerprint onto the session. So the widget posts and the board posts from one browser become one person record, and a climb from then on carries both: sign in with Google after leaving three notes through the widget and all three are credited to you, not to Anonymous. Later widget posts from that browser arrive credited too, without anyone doing anything.

Two limits worth knowing. The bridge is one-way and one-time per fingerprint: the first person to claim it keeps it, so a shared browser does not shuffle posts between visitors. And it only reaches the board it is named after — a person who uses the widget on two of your products bridges each one on its own board. The public page covers what they find when they get there.

Spam and rate limits

Two defenses, both invisible to real people.

The honeypot is the hidden field. Bots fill in every input they find; people cannot see this one. A submission arriving with anything in it is rejected.

Rate limits are a fixed one-hour window, counted two ways: 20 submissions per fingerprint and 60 per IP address. The budget is shared with plus-ones and replies made on the board from the same browser, so a bot cannot dodge it by alternating actions.

Both rejections look the same from the outside — the panel says Could not send. Try again? and nothing indicates which limit was hit. That is on purpose: a terse, uniform failure gives an abusive script nothing to tune against. The cost is that a person who trips a limit sees the same message as a person whose network dropped.

Content Security Policy

If your app sends a Content-Security-Policy header, allow the API host in two directives — one to load the script, one to let it post:

Content-Security-Policy: script-src 'self' https://api.convo.randomfact.com; connect-src 'self' https://api.convo.randomfact.com

Nothing else is needed. The widget loads no fonts, no images, and no stylesheets, and it makes exactly one request: the submission itself.

Caching

widget.js is served with cache-control: public, max-age=3600, so browsers and CDNs hold it for an hour. There is no version in the URL — you always get the current widget, within that hour.

What the widget cannot do

The limits are real, and they are the reason the next page exists.

  • Light theme only. The panel is white with dark text, whatever your app's theme is.
  • A fixed corner. Bottom-right, always. You cannot anchor it to your own button or open it from your own code.
  • Capture only. No feed, no replies, no plus-ones, no roadmap, no changelog — those live on the board.
  • No identity rungs. Nobody can sign in from the panel, so a widget post is anonymous until its author visits the board — and until then Convo has no way to tell them when their request ships.
  • No duplicate prompt. The board's capture box offers similar existing feedback as you type; the widget does not, so widget posts are more likely to duplicate something already on the board.

If any of those matter, build your own capture UI — same endpoint, same spam machinery, your component. Wire Convo into your product compares all four routes.

A complete page

Everything above, in a file you can open:

<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<title>Acme</title>
	</head>
	<body>
		<h1>Acme</h1>
		<p>Your app, with nothing else on the page.</p>

		<script
			src="https://api.convo.randomfact.com/widget.js"
			data-board="acme-co/WEB"
			async
		></script>
	</body>
</html>

Test it

Load the page and check three things in order.

  1. The launcher appears in the bottom-right corner with your label on it. If it does not, open the console: a missing or malformed data-board is reported there as [convo] data-board must be "workspace/KEY".
  2. A post succeeds. Type a sentence, choose Send, and watch for Thanks — posted.
  3. It arrives. Open /{workspace}/{KEY} in another tab before you send. The board is live, so your post appears at the top of the feed without a refresh, credited to "Anonymous".

If step 2 fails, the usual causes are a product that is not public, a data-board naming a workspace that does not exist, and a Content Security Policy missing connect-src. The console will show the CSP one; the other two look identical from the widget, so check the board's own URL loads first.