fdbck.

Board API

Render your public feedback board however you like — your markup, your styles, your framework. This is a small, CORS-enabled JSON API: read the sorted items, cast and retract votes, and check which items a visitor already voted for. No SDK, no iframe, no fdbck chrome. If you just want a board without writing code, the hosted board and its iframe embed are one toggle away.

Only ever display-safe data. Every response returns exactly what the hosted board shows — the owner-published title (or a PII-scrubbed summary), the public status, and the vote count. The raw submitted message, submitter email, and page context are never exposed. You can safely render responses straight into your page.

Authentication

Auth is your public widget key (wk_…), passed as the key query param (or in the JSON body for votes). It's the same key your widget embed uses and is meant to be public — it only identifies which board to read and can't read anything private. The board must be enabled in Settings → Public board first.

Endpoints

Read the board

http
GET https://fdbck.app/api/v1/board?key=wk_your_key_here&sort=trending

Query parameters:

ParamTypeNotes
keystring · requiredYour public widget key (wk_…), the same one the widget embed uses. Safe to expose in client-side code.
sort"trending" | "top" | "new"Order. Default trending — momentum (votes ÷ days on the board), so a hot new item outranks a stale high-vote one. top = most votes all-time. new = most recently published.
statuscomma list · optionalFilter to public statuses: planned, reviewing, in_progress, shipped, not_planned. planned is an alias covering new + under-review items. Unknown tokens are ignored. Omit for everything.
limit1–100 · optionalMax rows (default and hard cap 100).

Returns { sort, status, limit, count, items: [] }, cached at the edge for ~30s. Each item:

FieldTypeNotes
idstring (uuid)Stable item id. Use it to vote and to key your DOM rows.
titlestringThe display title: the owner's public headline, or a PII-scrubbed one-line summary. Never the raw submitted message.
type"bug" | "feature" | "other" | nullTriage's classification, or null if not yet triaged.
status"received" | "reviewing" | "in_progress" | "shipped" | "not_planned"The public lifecycle status. Map these to your own column labels/colors.
votesnumberCurrent upvote count.
publishedAtstring (ISO 8601) · nullableWhen the owner published the item to the board. Use for a relative timestamp.
200 OK
{
  "sort": "trending",
  "status": null,
  "limit": 100,
  "count": 2,
  "items": [
    {
      "id": "9c1e…",
      "title": "Bulk export to CSV",
      "type": "feature",
      "status": "received",
      "votes": 12,
      "publishedAt": "2026-07-02T18:04:11.000Z"
    },
    {
      "id": "4a7f…",
      "title": "Dark mode for the dashboard",
      "type": "feature",
      "status": "in_progress",
      "votes": 42,
      "publishedAt": "2025-05-30T19:28:57.000Z"
    }
  ]
}

Cast or retract a vote

http
POST https://fdbck.app/api/v1/board/vote
Content-Type: application/json

{ "key": "wk_your_key_here", "itemId": "9c1e…", "voterKey": "…", "direction": "up" }

Body fields:

FieldTypeNotes
keystring · requiredYour widget key.
itemIdstring (uuid) · requiredThe item to vote on. Must belong to this board and be published.
voterKeystring · 8–128 · requiredAn opaque token YOU mint and persist per visitor (see the voter-key contract). Dedups + lets a vote be retracted. Not a secret, not PII.
direction"up" | "down"Default "up" (cast a vote). "down" retracts this voter's own vote. Idempotent: a repeat "up" is a no-op, not a double count.

Returns { votes, voted } — the authoritative new count and whether this voter now has a vote on the item. Reconcile your optimistic UI to votes.

Check a visitor's votes

http
GET https://fdbck.app/api/v1/board/votes?key=wk_your_key_here&voterKey=…

Returns { votedItemIds: [] } — the ids on this board that this voter key has upvoted, so you can render each control in its pressed state on load. Scoped to this board only.

Subscribe to ship notifications

http
POST https://fdbck.app/api/v1/board/subscribe
Content-Type: application/json

{ "key": "wk_your_key_here", "itemId": "9c1e…", "email": "you@example.com", "voterKey": "…" }

Lets a visitor ask to be emailed when an item ships (its fix PR merges). Returns { ok: true }. email is required; voterKey is optional. Idempotent per (item, email) — re-subscribing just clears any prior unsubscribe. The item must be published on this board. Rate-limited to 20 requests/min per IP. Every notification email carries a one-click unsubscribe link (GET /api/v1/board/unsubscribe?token=…), so you don't manage opt-outs yourself. The email shows only the public title and scrubbed summary — never a reporter's raw text.

The voter-key contract

Voting is anonymous — no accounts, no sign-in. To dedup votes and let people retract them, YOU generate one opaque token per visitor and send it as voterKey on every vote and vote-check. Mint it once and persist it (e.g. localStorage); reuse the same value for that visitor forever.

  • It's not a secret and not PII — just a random 8–128-char string. Knowing someone's key reveals only their public votes.
  • One vote per (item, voterKey) is enforced server-side, so a repeated up-vote can't double-count.
  • A new key = a fresh voter. If a visitor clears storage they get a new key and can vote again — acceptable for an open, anonymous board. If you need stronger guarantees, gate voting behind your own auth and derive the key from your user id.
mint a voter key
function voterKey() {
  let v = localStorage.getItem("myapp_voter");
  if (!v) {
    v = crypto.randomUUID() + crypto.randomUUID();
    localStorage.setItem("myapp_voter", v);
  }
  return v;
}

CORS & rate limits

Every endpoint sends Access-Control-Allow-Origin: * and answers preflight, so you can call it directly from any origin's browser JS — no proxy needed. Limits (per minute): reads 60/key, votes 20/voter and 60/IP (a vote must clear both), vote-checks 60/voter. Over any limit returns 429 rate_limited. The board read is edge-cached ~30s, so real traffic rarely touches its limit (vote and vote-check are never cached).

Errors

Errors return a JSON { error, hint } body with the matching status:

StatuserrorWhen
400missing_key / invalid_voter_key / invalid_payloadA required param is missing or malformed (bad JSON body, voterKey out of the 8–128 range, etc.).
404not_foundNo public board for this key — either the key is wrong or the board isn't enabled in Settings → Public board. (The API doesn't reveal which, to avoid leaking whether a key exists.)
429rate_limitedOver the per-key / per-voter limit. Back off and retry.

Full example

A complete board — read, render in your own markup, show the visitor's existing votes, and toggle a vote — in about 40 lines of vanilla JS. Style the .my-* classes however you like.

index.html
<div id="board"></div>
<script>
const KEY = "wk_your_key_here";
const API = "https://fdbck.app/api/v1";

// 1. A stable, per-visitor voter key — mint once, keep it in localStorage.
function voterKey() {
  let v = localStorage.getItem("myapp_voter");
  if (!v) { v = crypto.randomUUID() + crypto.randomUUID();
            localStorage.setItem("myapp_voter", v); }
  return v;
}

async function render(sort = "trending") {
  // 2. Read the board + this visitor's existing votes in parallel.
  const [board, mine] = await Promise.all([
    fetch(`${API}/board?key=${KEY}&sort=${sort}`).then(r => r.json()),
    fetch(`${API}/board/votes?key=${KEY}&voterKey=${voterKey()}`).then(r => r.json()),
  ]);
  const voted = new Set(mine.votedItemIds);

  // 3. Render in YOUR markup + styles. Nothing here is fdbck's design.
  document.getElementById("board").innerHTML = board.items.map(it => `
    <article class="my-card">
      <button class="my-vote ${voted.has(it.id) ? "is-on" : ""}"
              data-id="${it.id}">▲ ${it.votes}</button>
      <div>
        <h3>${it.title}</h3>
        <span class="my-badge my-${it.status}">${it.status}</span>
      </div>
    </article>`).join("");
}

// 4. Toggle a vote, then re-render to reflect the authoritative count.
document.getElementById("board").addEventListener("click", async (e) => {
  const btn = e.target.closest(".my-vote");
  if (!btn) return;
  const direction = btn.classList.contains("is-on") ? "down" : "up";
  await fetch(`${API}/board/vote`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ key: KEY, itemId: btn.dataset.id, voterKey: voterKey(), direction }),
  });
  render();
});

render();
</script>

For the submission side (letting people file feedback from your own UI), see the Feedback API. To let an AI agent read and act on your feedback, see the MCP server.