API & webhooks

Feedback doesn't have to come from the widget, and it doesn't have to stay in Voicebox.

Submitting feedback

The same endpoint the widget uses. It takes your publishable key, so it's safe from a browser. Pipe in support tickets, app-store reviews, or anything else you already collect.

POST /api/ingest
curl -X POST https://www.usevoicebox.dev/api/ingest \
  -H "Content-Type: application/json" \
  -d '{
    "key": "pk_your_project_key",
    "body": "The CSV export times out on large ranges.",
    "type": "ISSUE",
    "rating": 2,
    "email": "user@example.com",
    "pageUrl": "https://app.example.com/reports",
    "metadata": { "plan": "pro", "userId": "usr_8123" }
  }'
Fields
key
required, your publishable project key
body
required, the feedback text, up to 5,000 characters
type
IDEA · ISSUE · PRAISE · QUESTION · OTHER (default OTHER)
rating
optional integer, 1 to 5
email
optional, never sent to the model
pageUrl, locale, referrer
optional context
metadata
optional object, never sent to the model

Reading data back

Create a secret key under Settings → Developers, available on Pro and above. It's shown once and stored only as a hash, so keep it somewhere safe. Send it as a bearer token.

GET/api/v1/feedback

Newest first. Filter by project_id, status, type, sentiment, since.

GET/api/v1/feedback/:id

A single submission with its theme attached.

GET/api/v1/themes

Ordered by priority, so the first item is what to work on next.

GET/api/v1/projects

Every project in your organization, with its widget key.

GET /api/v1/themes
curl "https://www.usevoicebox.dev/api/v1/themes?limit=2" \
  -H "Authorization: Bearer sk_your_secret_key"

{
  "data": [
    {
      "id": "cm4x8k2p90001",
      "project_id": "cm4x8k1a70000",
      "title": "CSV export times out on large ranges",
      "description": "Exports of more than six months fail silently.",
      "sentiment": "NEGATIVE",
      "item_count": 34,
      "negative_share": 0.82,
      "priority_score": 3.41,
      "status": "ACTIVE",
      "last_seen_at": "2026-08-11T09:14:22.000Z"
    }
  ],
  "has_more": true,
  "next_cursor": "cm4x8k2p90001"
}

Paging

Pass next_cursor back as ?cursor= to get the following page. Cursors rather than offsets, because feedback arrives while you're paging and an offset would quietly skip rows. limit tops out at 100.

Errors

Every failure is a JSON body with a stable error.code and a message written for a human reading a log.

401 Unauthorized
{
  "error": {
    "code": "invalid_key",
    "message": "That API key is not valid."
  }
}

Model Context Protocol

The same read access, but for your coding agent. Point Claude Code, Cursor, or any MCP client at Voicebox and it can pull your ranked themes straight into wherever you write code, so “what should I build next” is answered from what your users actually said. It's a remote server, so there's nothing to install.

Add it to Claude Code
claude mcp add --transport http voicebox \
  https://www.usevoicebox.dev/api/mcp \
  --header "Authorization: Bearer sk_your_secret_key"

Same secret key as the API above, so it's behind the same plan and the same rate limit, and revoking the key cuts off the agent too. Every tool is read-only, an agent can read your feedback but never change it.

list_projectsYour projects, to get a project_id.
list_themesThe ranked list of what to work on, top item first.
get_themeOne theme plus the real messages behind it.
list_feedbackRaw feedback, filterable by type, sentiment, and date.
project_overviewTotals, sentiment split, and negative share.

Webhooks

Point Voicebox at an HTTPS URL under Settings → Developers and get a signed POST when something happens. Useful for routing angry feedback into Slack the moment it lands, or opening a ticket when a new theme appears.

  • feedback.createdFeedback arrives
  • feedback.analyzedSentiment and summary are ready
  • theme.createdA new theme is identified
POST your-endpoint
Voicebox-Event: feedback.analyzed
Voicebox-Signature: t=1786521600,v1=8f3c…

{
  "event": "feedback.analyzed",
  "createdAt": "2026-08-11T09:14:22.000Z",
  "data": {
    "id": "cm4x8k9d10007",
    "project_id": "cm4x8k1a70000",
    "body": "The CSV export times out on large ranges.",
    "type": "ISSUE",
    "sentiment": "NEGATIVE",
    "sentiment_score": -0.7,
    "category": "Data export",
    "summary": "Export fails on ranges longer than six months.",
    "theme": { "id": "cm4x8k2p90001", "title": "CSV export times out" }
  }
}

Verifying the signature

The timestamp is inside the signed string, so a captured request can't be replayed at you later. Compare in constant time and reject anything older than five minutes.

verify.ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(header: string, rawBody: string, secret: string) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=")),
  );

  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (age > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest();

  const received = Buffer.from(parts.v1, "hex");
  return (
    expected.length === received.length &&
    timingSafeEqual(expected, received)
  );
}

Retries. A delivery times out after 8 seconds and any non-2xx counts as a failure. We don't replay individual events, but an endpoint that fails twelve times in a row is switched off rather than hammered, and you'll see the last status code in settings. Turn it back on once it's fixed and the counter resets.

Rate limits

Ten submissions per IP per hour per project on ingest. Over-quota accounts keep accepting feedback up to a hard ceiling, we won't discard your users' words because of a billing state, but new items stop being analyzed until the period resets or you upgrade.