theyasked.me

Docs

Vote lists

“Vote or request an integration”: a short list with logos, a Vote button and an inline request field, in the widget, inline, in a WebView or headless.

A Vote list is a small, curated vote widget for one decision: “Vote or request an integration”, “Which template next?”, “Which language should we add?”. It shows a short list of options, each with a logo tile, a name, a one-line summary and a Vote button, plus an inline “Request another…” field. You put it exactly where the question comes up in your product: next to your integrations page, in onboarding, in a mobile settings screen.

Everything in a list is an ordinary request on one of your boards, so votes, voters, revenue, statuses, notifications, webhooks, insights and MCP all work as usual. Votes and requests that come through a list are tagged with the source Vote list (vote_list).

How it works

  • A list reads the requests of one board (an Integrations board, say). Closed requests never show; shipped ones are hidden unless you turn that off; team-only and unapproved ones are never public.
  • Order: manual (drag in the builder), most voted or newest; at most N items.
  • Each request carries an optional icon (a website's logo, an emoji, or an uploaded image) and a summary (one plain-text line, up to 140 characters). Without an icon the tile shows the first letter of the name.
  • Request another… looks for the same name on the board first (case, accents, spaces and words like “integration” don't matter: “woo commerce” is WooCommerce). When it exists, the person's vote goes there (“Already requested, we added your vote”); otherwise a new request is created on the board (“Added, we'll let you know when it ships”). Your workspace's rules apply: moderation (new requests wait for approval), anonymous voting / posting, rate limits, and the board must accept new posts.
  • People who vote or request follow the request: they hear from you when it ships.

The integrations example, end to end

1. A board

Settings → Boards → New board “Integrations”. Turn off Show on the portal only if you don't want it on the public portal at all: a list needs its board on the portal (the votes go through the public API).

2. Requests with logos

Create one request per integration (Inbox → New post, or the REST API / MCP). On each request page, Vote list card:

  • Website: type the domain (shopify.com, salla.sa). TheyAsked fetches the site's icon once (its apple-touch-icon, the <link rel="icon"> it declares, or /favicon.ico, the best one of 64 px or more) and keeps a copy. If the site has nothing usable, a letter tile is shown.
  • Emoji: pick one from the grid or paste your own.
  • Upload: a square image, at least 64 px (PNG, JPEG, WebP or SVG).
  • Summary: one line under the name, e.g. “Sync orders, customers and refunds”.

From an AI assistant (MCP): update_post with { "post": "shopify", "icon": { "kind": "domain", "value": "shopify.com" }, "summary": "Sync orders and customers" }.

3. The list

Settings → Vote lists → New list: pick the board, a title (“Vote or request an integration”), an optional eyebrow (“Integrations”) and a key (integrations). The builder then lets you:

  • edit the subtitle, the request field's placeholder and button label, and the list's own icon;
  • choose the order, how many to show, whether to hide shipped ones, show vote counts or status, and whether to take new requests;
  • drag the requests into order (or focus a handle and use ↑ / ↓);
  • preview the real list in light or dark, as a phone or as the widget panel.

4. Put it in your product

Install the widget once (WIDGET.md) and identify signed-in users with SSO (SSO.md), then pick one of:

A button that opens the list in the widget panel (a full-screen sheet on phones):

<button type="button" onclick="TheyAsked('open', { list: 'integrations' })">
  Vote or request an integration
</button>

Inline, in the page (the list sizes itself to its content; any number per page, also when your app renders them later):

<div data-theyasked-list="integrations"></div>

Mobile WebView, presented as a modal sheet with your own ✕ (MOBILE.md):

https://theyasked.me/embed/<workspace>?list=integrations&platform=ios&hideHeader=1&token=<sso-jwt>

Headless JS (no UI; draw it yourself, same identity as the widget):

window.addEventListener('theyasked:loaded', async () => {
  const { list, items } = await TheyAsked.list('integrations');
  // items: [{ id, slug, title, summary, icon, iconUrl, viewerVoted, voteCount?, status? }]
  await TheyAsked.vote(items[0].id); // TheyAsked.unvote(id) takes it back
  const r = await TheyAsked.request('integrations', 'Salla');
  if (r.matched) console.log('Already requested; vote added to', r.matched.title);
  else console.log('Created', r.post.title, r.pendingModeration ? '(waits for review)' : '');
});

Errors reject with { status, code }: unauthorized (sign-in needed: anonymous voting or posting is off), forbidden (the list doesn't take requests), not_found, rate_limited. Types: https://theyasked.me/widget.d.ts (TheyAskedHeadless, TheyAskedListItem, …).

5. Events

Where Event Payload
Widget TheyAsked('on', 'voted', fn) { postId, voted, voteCount, listKey }
Widget TheyAsked('on', 'requested', fn) { listKey, postId, title, matched, pending }
WebView bridge theyasked:voted same as above
WebView bridge theyasked:requested same as above
WebView bridge theyasked:ready { workspace, platform, tabs: [], list }

back (or TheyAskedEmbed.back()) on a list emits theyasked:close: the list is the root view, so your app dismisses its sheet.

6. Analytics

  • Insights → Sources: votes and new requests from lists show as Vote list; the inbox and insights filters take Source: Vote list.
  • Each request's Who asked panel lists the voters with their source, company, plan and revenue, like any other vote.
  • Exports (posts.csv, votes.csv) carry the source, and posts also their summary and icon.
  • Webhooks: post.created / vote.created with source: "vote_list" and the list key.

API

Public (end-user, /api/v1/w/:ws, same session as the widget / embed):

Method Path
GET /lists/:key { list, items }; voteCount / status only when the list shows them. Viewer-independent part edge-cached 15 s; viewerVoted per session.
POST /lists/:key/requests { title } → 201 { post, pendingModeration } or 200 { matched, pendingModeration: false }
POST / DELETE /posts/:id/vote Voting, with X-TheyAsked-Source: vote_list
GET /logos/:domain The logo of a domain icon this workspace uses (a lettermark SVG when the site has none; ?fallback=none answers 404 instead). Other domains: 404.

Dashboard (/api/admin, admin or owner): GET|POST /vote-lists, GET|PATCH|DELETE /vote-lists/:id (PATCH is partial: absent fields stay), POST /vote-lists/:id/reorder { itemOrder }, and GET /logos/:domain (preview for the icon editor). Post icons and summaries: PATCH /api/admin/posts/:id { icon, summary }. Details: API.md.

Public REST API (workspace API key, /api/public/v1): GET|POST /vote-lists, GET|PATCH /vote-lists/{id} (id or key; board by id, slug or name, itemOrder by request id or slug; admin role and the voteLists plan flag for writes). Each list comes with its snippets (widget, inline, webview).

MCP: list_vote_lists (read), create_vote_list / update_vote_list (posts:write, admin role, audit-logged; same fields as the dashboard, partial update by id or key; the result has the list, what it shows now and the three snippets), and icon / summary on create_post / update_post. An assistant can set up the whole integrations example: create_post per integration with an icon and summary, then create_vote_list { key: "integrations", board: "Integrations", title: "Vote or request an integration" }.

Plans

Vote lists are on every plan (plan flag voteLists, editable in /platform/plans). Turning it off pauses them: lists can't be created or edited and stop being served (404); nothing is deleted.

Logos: what is fetched, and how safely

Only domains used as an icon in your workspace are ever fetched, from the Worker, never from your users' browsers. Every request (and every redirect) is checked: https only, no private, loopback, link-local or cloud-metadata addresses, at most 3 redirects, 5 requests and 256 KB per logo, 5 s timeouts. The bytes decide the type: PNG, JPEG, GIF, WebP, ICO, or an SVG without scripts, event handlers, external references or entities. The result is stored in your workspace's file storage (deleted with the workspace) and served with nosniff and a sandbox CSP; a site without a usable logo is retried after a week. Without file storage (R2) on a deployment, tiles show the letter instead.