> voicepanel mcp / docs
Connect Voicepanel to your AI assistant
One connector gives your assistant your studies, your participants, and your findings — designing research, launching it, and reading the results without leaving the conversation.
Server URL
https://app.voicepanel.co/api/v1/mcp
On this page
Overview
Voicepanel runs AI-moderated voice interviews with real people. Our MCP server exposes that platform as tools your assistant can call directly — so you can design a study, choose how to collect responses, and read what participants said, all in one conversation.
It works with any client that supports remote MCP servers over HTTP with OAuth; Claude and Claude Code are the ones we test against. There is no API key to manage and no separate charge — connector access is included with your Voicepanel subscription, and your existing role governs what it can do.
Connect
Every client needs the same thing: the server URL, followed by a browser login to Voicepanel.
https://app.voicepanel.co/api/v1/mcp
Claude (web and desktop)
- Open
Settings→Connectors. - Choose
Add custom connectorand paste the server URL. - Click
Connectand complete the Voicepanel login that opens.
Claude Code
Add the server, then authenticate from the CLI:
claude mcp add --transport http voicepanel https://app.voicepanel.co/api/v1/mcp
Run /mcp inside Claude Code and select Voicepanel to start the login. If you already connected Voicepanel in Claude, there is nothing to add — Install in Claude Code covers that, along with scopes, sharing the server with your team, and headless runs.
Other MCP clients
Claude and Claude Code are the clients we test against. Any other client that supports remote MCP servers over HTTP with OAuth connects the same way — add a remote server with the URL above. The server supports dynamic client registration, so most clients need nothing beyond the URL to begin the OAuth flow.
Install in Claude Code
There are two ways to get Voicepanel into Claude Code. If you have already connected Voicepanel in Claude, there is nothing to install. Otherwise, add the server to the CLI once.
Already connected in Claude? Nothing to install
Voicepanel is listed in the Claude connector directory, and Claude Code reads the connectors on your Claude account. Connect Voicepanel once under Settings → Connectors in Claude, then run /mcp in Claude Code — Voicepanel is in the list, marked as coming from claude.ai, already signed in. The same connection follows you into every project on every machine you log into.
This applies to Claude subscription logins only
Claude Code loads claude.ai connectors when it is authenticated with your Claude account. A session running on an Anthropic API key, Amazon Bedrock, or Google Cloud does not see them — run /status to check which is active, and add the server directly instead.
Add the server directly
claude mcp add --transport http voicepanel https://app.voicepanel.co/api/v1/mcp
That writes the server at local scope — the current project only, private to you. Add --scope user to load it in all of your projects, or --scope project to share it with your team through a .mcp.json file at the repository root:
claude mcp add --transport http --scope project voicepanel https://app.voicepanel.co/api/v1/mcp
The committed entry looks like this. Each teammate approves the file once and signs in with their own Voicepanel account, so nothing secret lands in version control:
{
"mcpServers": {
"voicepanel": {
"type": "http",
"url": "https://app.voicepanel.co/api/v1/mcp"
}
}
}Sign in
Adding the server writes configuration; it does not authenticate.
A project-scoped server needs approving first. Start claude in the repository and accept the prompt to trust the workspace and its servers — until you do, Voicepanel shows as ⏸ Pending approval and Claude Code will not connect to it.
Then run /mcp, select voicepanel, and finish the Voicepanel login in the browser. This is the path that works at every scope.
For a server added at local or user scope, you can sign in straight from the shell instead. On a machine with no browser — an SSH session, a container — add --no-browser: Claude Code prints the authorization URL, you open it locally, and paste the full redirect URL back at the prompt.
claude mcp login voicepanel --no-browser
If that command answers that the server does not support OAuth login, use /mcp — the panel runs the same flow.
Voicepanel supports dynamic client registration, so there is no client ID or secret to create beforehand and no callback port to pin. Claude Code stores the tokens and refreshes them for you.
Check that it worked
claude mcp list
Voicepanel should read ✔ Connected. If it reads ! Needs authentication, the sign-in above has not been completed. Then ask for something small — “List my Voicepanel studies” — to confirm the tools are live.
Managing the server
claude mcp get voicepanel— scope, URL, and connection status.claude mcp logout voicepanel— clear the stored credentials without removing the server.claude mcp remove voicepanel— remove it entirely./mcp— re-authenticate, browse the tool list, or toggle the server off for one project.
Scripts and CI
A non-interactive run — claude -p, or the Agent SDK — cannot open a browser, so it cannot start the OAuth flow. Sign in once interactively as the user that will run the job, and the stored credentials carry over. Refresh tokens last 30 days and rotate on use, so a runner that sits idle longer than that needs a fresh login.
Authentication
Connecting opens a standard OAuth flow in your browser. Sign in the way you always do — Google, SAML single sign-on, or a magic link — and approve the connection on the consent screen.
If you belong to more than one Voicepanel account, the consent screen asks which one to connect. The connector is bound to that account: to work in a different one, disconnect and connect again, choosing the other account.
Access tokens last one hour and refresh automatically in the background. Refresh tokens last 30 days and rotate each time they are used. Disconnecting the connector in your client revokes them.
Permissions are enforced in the database
Filtering the tool list by role is a convenience. Every query the connector makes runs through the same row-level security policies as the Voicepanel app, so an assistant cannot reach data your account could not open in the product itself.
Roles & permissions
The connector sees exactly what your Voicepanel role allows. Admins and editors get all 33 tools. Viewers get 18 of them — an assistant connected as a viewer can explore and summarize research, but cannot change anything.
Read and write are marked per tool below, and they are not the same as the role split: suggest_panel_provider changes nothing, but it runs an AI call, so it needs an editor or admin like the write tools do.
| Role | Tools available |
|---|---|
| Admin | All 33 — read and write |
| Editor | All 33 — read and write |
| Viewer | 18 — everything that only reads data |
Tools
37 tools, grouped by what you are trying to do. Expand any tool for its parameters and the shape it returns.
Panel publishing is not yet enabled over MCP
Your assistant can draft a panel recruit — targeting, screener, incentive and all — but publishing one still happens in the Voicepanel app. Link and intercept distributions publish normally through the connector.
Set up
Learn how a good study is put together, then design it and the media it shows participants.
Account context
Get current account information (name, subscription tier, credits, limits)
Parameters
None — this tool reads from your connected account.
Returns
id
string
name
string
subscription_tier
string
panel_credits_in_cents
number
max_admins
number
max_study_length_min
number
max_project_cost_in_cents
number
created_at
string
Get current user profile (name, email, role in account)
Parameters
None — this tool reads from your connected account.
Returns
id
string
first_name
string
last_name
string
string
role
string
Design guidance
How to design a good Voicepanel study, and how to express it as a study plan. Covers research best practices (including this account's own AI rules, if it has customized them), every step and stimulus type with its exact fields, the template variables you can interpolate into question text and stimulus URLs, conditional sections, section randomization, and the constraints that fail validation. Call this before writing or editing a plan by hand — generate_study already applies this guidance internally. Read-only. Studies are also called "projects".
Parameters
None — this tool reads from your connected account.
Returns
guide
string
account_rules_applied
boolean
template_variables
object[]
id
string
label
string
description
string
contexts
string[]
qualification_tags
object[]
id
string
applies_to
string
description
string
signal_logic
object[]
id
string
description
string
Example
Before hand-authoring or editing a study plan, so you write it against the same research rules and plan constraints the dashboard applies — and so you know about fields no tool description mentions, like template variables and conditional sections.
Arguments
{}Response
{
"guide": "# Designing a Voicepanel study\n\n## What a study is\n\nA study plan is sections, each holding an ordered list of steps…\n\n## Study authoring rules (this account's own rules)\n\nThis account has replaced the default rules below…",
"account_rules_applied": true,
"template_variables": [
{
"id": "response_number",
"label": "Response number",
"description": "The respondent's sequential number in the study",
"contexts": ["question_text", "stimulus_url"]
},
{
"id": "<section>.<step>",
"label": "Previous answer",
"description": "Pipes an earlier answer into this question, both numbers 1-based — {{1.2}} is the second step of the first section.",
"contexts": ["question_text", "stimulus_url"]
}
]
}Takes no arguments and changes nothing. guide is markdown, elided here — the real payload runs a few thousand words. account_rules_applied is true when this account has customized its AI rulesets: the guide then carries that account's rules in place of the defaults, matching what generate_study writes by and review_study grades against. template_variables repeats the guide's variable reference as structured data, so a client can enumerate it without parsing prose; each entry lists the contexts it resolves in, since question text, stimulus URLs, and redirect URLs each build a different set.
How to write screener questions that qualify the right respondents and resist gaming. Covers the qualification tags and the must_select/must_one_of trap, obfuscating the target so respondents can't guess the qualifying answer, honeypot options, option randomization and anchoring, early screenout, question order, and how to write open-ended and matrix questions. Includes this account's own AI rules if it has customized them. Call this before passing screener_questions to a distribution tool by hand — passing an audience description instead applies this guidance automatically. Read-only.
Parameters
None — this tool reads from your connected account.
Returns
guide
string
account_rules_applied
boolean
template_variables
object[]
id
string
label
string
description
string
contexts
string[]
qualification_tags
object[]
id
string
applies_to
string
description
string
signal_logic
object[]
id
string
description
string
Example
Before passing `screener_questions` to a distribution tool by hand. Passing an `audience` description instead applies the same guidance automatically.
Arguments
{}Response
{
"guide": "# Designing a Voicepanel screener\n\n## What a screener is\n\nA screener is a set of questions on a distribution…",
"account_rules_applied": false,
"qualification_tags": [
{
"id": "must_one_of",
"applies_to": "checkbox",
"description": "At least one option in this group must be checked to qualify. The right tag when several answers are acceptable alternatives — the most common positive qualifier."
},
{
"id": "must_select",
"applies_to": "checkbox",
"description": "This specific option must be checked to qualify. Tagging several options this way requires ALL of them, which is rarely the intent."
}
]
}Takes no arguments and changes nothing. The guide covers the anti-gaming design the platform's own screener generator applies — obfuscating the target so respondents can't guess the qualifying answer, honeypot options, option randomization — plus the mechanics of the qualification tags. qualification_tags is elided here; the full response lists all five.
Studies
List the active studies in the account, with response counts and a dashboard url per study. internal_name is the project name as it appears in the dashboard. Archived studies are excluded. Studies are also called "projects".
Parameters
None — this tool reads from your connected account.
Returns
studies
object[]
id
string
internal_name
string
status
string
objective
string
created_at
string
response_count
number
distribution_count
number
in_review_distribution_count
number
computed_status
string
Get detailed study information including questions, plan, settings, and response statistics, plus url, a dashboard link to open the study. Studies are also called "projects".
Parameters
study_id
string
required
The study ID
Returns
id
string
url
string
account_id
string
display_name
string
internal_name
string
status
string
objective
string
context
string
questions
any
plan
any
plan_source
string
welcome_text
string
show_welcome_page
boolean
target_length_min
number
written_in_language
string
template_id
string
brand_words
any
enable_tts_default
boolean
tts_voice_id
string
uniqueness_constraint
string
tags
any
source
string
created_at
string
updated_at
string
stats
any
data
string
Example
You have a study ID and need its full configuration — every section, step, and setting — plus how many people have viewed, started, and completed it.
Arguments
{
"study_id": "stdy_7Kq2mXbA9fLpZv"
}Response
{
"id": "stdy_7Kq2mXbA9fLpZv",
"display_name": "Checkout drop-off — Freshline mobile app",
"internal_name": "Q3 checkout drop-off",
"objective": "Understand why Freshline shoppers abandon their cart at the delivery-slot step, and what would get them to complete the order.",
"context": "Freshline is a same-day grocery delivery app. Since the March checkout redesign, 38% of carts are abandoned on the delivery-slot screen.",
"questions": [],
"plan": {
"version": "2025_04",
"sections": [
{
"intro": "First, a bit about how you shop for groceries.",
"steps": [
{
"question": "Walk me through the last time you ordered groceries for delivery. What were you trying to get done?",
"type": "long_question",
"probing_areas": "What triggered the order, how much of a hurry they were in, which app they reached for first",
"probing_type": "custom",
"response_format": "audio"
},
{
"question": "How often do you order groceries for delivery?",
"type": "single_select",
"options": [
{ "value": "A few times a week", "anchor": null },
{ "value": "About once a week", "anchor": null },
{ "value": "A few times a month", "anchor": null },
{ "value": "Less often than that", "anchor": null }
],
"shuffle_options": false,
"vertically_align_choices": true
}
],
"stimulus": null,
"recording": null
},
{
"intro": "Now I'd like you to try the new checkout flow.",
"steps": [
{
"question": "Open the prototype and book a delivery slot for tomorrow evening, then place the order.",
"type": "task",
"task_type": "generic_instruction",
"expected_duration_seconds": 120
},
{
"question": "How easy or hard was it to choose a delivery slot?",
"type": "rating_scale",
"options": [
{ "value": "1", "anchor": null },
{ "value": "2", "anchor": null },
{ "value": "3", "anchor": null },
{ "value": "4", "anchor": null },
{ "value": "5", "anchor": null }
],
"lowest_rating_label": "Very hard",
"highest_rating_label": "Very easy"
}
],
"stimulus": {
"type": "prototype",
"url": "https://www.figma.com/proto/8JxQ0eR2mN/Freshline-checkout-v3",
"name": "Freshline checkout v3",
"placement": "throughout",
"sizing": "fit"
},
"recording": {
"audio": true,
"video": false,
"screen": true,
"mobile_screen": false
}
}
],
"randomizations": [],
"device": "desktop"
},
"plan_source": "study.plan",
"welcome_text": "Thanks for making time for this. We'll spend about 8 minutes on how you shop for groceries and on a checkout flow we're testing — there are no right answers, we just want your honest reactions.",
"target_length_min": 8,
"written_in_language": "en",
"created_at": "2026-07-14T16:32:07.418+00:00",
"updated_at": "2026-07-21T09:05:44.902+00:00",
"stats": {
"views": 214,
"starts": 168,
"completes": 131,
"analyzed": 129,
"averageDuration": 447.6
},
"url": "https://app.voicepanel.com/accounts/acct_R4vN8sPqKdT2yE/studies/stdy_7Kq2mXbA9fLpZv/create"
}study_id is the only parameter — there are no optional ones. Unlike the study write tools, get_study does not strip heavy fields, so plan is returned in full; this is the tool you call after create_study/edit_study/generate_study hand you the [omitted …] placeholder. The returned columns are exactly those fetchStudy selects, so account-level fields (account_id, tags, source) are not included. stats comes from the get_study_statistics RPC and always has these five keys; averageDuration is in seconds, over completed responses only. url is computed by the tool, not stored. If the study is large enough to exceed the 200K-char result cap, the payload is replaced by { "_truncated": true, "data": "<serialized prefix>" }.
List the templates saved in the account. Returns both distribution templates and screener templates; each row carries a type of 'distribution' or 'screener'.
Parameters
None — this tool reads from your connected account.
Returns
templates
object[]
List all labels in the account with the number of studies using each. Labels are stored as tags; a study has a single label. Studies are also called "projects".
Parameters
None — this tool reads from your connected account.
Returns
tags
object[]
id
string
name
string
count
number
Create a new study. If you are writing the plan yourself, call get_study_design_guide first — it covers the plan fields, the research rules this account applies, and the constraints that fail validation. Returns url, a dashboard link to open the study — share it with the user. Studies are also called "projects".
Parameters
name
string
required
The name for the study
objective
string
The research objective for the study
context
string
Additional context about the study
written_in_language
string
Language code for the study (default: 'en')
target_length_min
number
Target interview length in minutes
welcome_text
string
Welcome text shown to participants
internal_name
string
Internal name for the study (defaults to the display name)
show_welcome_page
boolean
Whether to show the welcome page to participants (default: true)
tag
string
Label to categorize the study (a study has a single label; created if it doesn't exist). See list_tags.
brand_words
string[]
Brand names and special terms to improve transcription accuracy
enable_tts_default
boolean
Read questions aloud to participants by default (text-to-speech)
tts_voice_id
string
Voice ID used for text-to-speech read-aloud. Options: Sarah (American, female) = EXAVITQu4vr4xnSDxMaL; Nora (Danish, female) = ONFS8Q3TuiPLQCXXa4dy; Lily (British, female) = pFZP5JQG7iQjIQuC4Bku; Katie (Irish, female) = sgk995upfe3tYLvoGcBN; Charlie (Australian, male) = IKne3meq5aSn9XLyUdCD; Daniel (British, male) = onwK4e9ZLuTAKqWW03F9; Adam (American, male) = pNInz6obpgDQGcFmaJgB; Michael (German, male) = 42I57fNB7wi2PIYWo0Xr.
EXAVITQu4vr4xnSDxMaL · ONFS8Q3TuiPLQCXXa4dy · pFZP5JQG7iQjIQuC4Bku · sgk995upfe3tYLvoGcBN · IKne3meq5aSn9XLyUdCD · onwK4e9ZLuTAKqWW03F9 · pNInz6obpgDQGcFmaJgB · 42I57fNB7wi2PIYWo0Xr
uniqueness_constraint
string
Restrict submissions by requiring a custom participant ID
custom_id_required · custom_id_required_single_submission
plan
object
Full study plan (sections, steps/questions, stimulus, recording, conditional branching, device). Provide this to create a study with content in one call; otherwise the study is created empty and must be filled in with edit_study or generate_study.
Returns
id
string
url
string
account_id
string
display_name
string
internal_name
string
status
string
objective
string
context
string
questions
any
plan
any
plan_source
string
welcome_text
string
show_welcome_page
boolean
target_length_min
number
written_in_language
string
template_id
string
brand_words
any
enable_tts_default
boolean
tts_voice_id
string
uniqueness_constraint
string
tags
any
source
string
created_at
string
updated_at
string
Generate a complete study using AI from a natural language description. The AI will create a full study plan with sections, questions, and conversation guides based on best practices in user research. Use this when you have a research objective or description and want the AI to design the study structure. Use create_study instead if you want to manually specify study fields. Returns the study with url, a dashboard link to open it — share it with the user. Studies are also called "projects".
Parameters
description
string
required
Natural language description of the study to generate (e.g. research objective, what you want to learn, target audience)
context
string
Additional context to inform the AI generation (e.g. brand guidelines, product details, prior research)
written_in_language
string
Language code for the study (default: 'en')
Returns
message
string
study
object
id
string
url
string
account_id
string
display_name
string
internal_name
string
status
string
objective
string
context
string
questions
any
plan
any
plan_source
string
welcome_text
string
show_welcome_page
boolean
target_length_min
number
written_in_language
string
template_id
string
brand_words
any
enable_tts_default
boolean
tts_voice_id
string
uniqueness_constraint
string
tags
any
source
string
created_at
string
updated_at
string
Example
You know what you want to learn but not how to structure the interview, and you want the AI to draft the whole study — sections, questions, and conversation guides — in one call.
Arguments
{
"description": "Find out why new users of Ledger, our personal budgeting app, stall at the bank-connection step of onboarding, and what would make them trust us enough to link a real account.",
"context": "Ledger connects to banks via Plaid. Onboarding is four screens: create account, connect bank, set goals, first budget. 41% of signups never finish the bank connection, and support tickets mention 'giving you my password'."
}Response
{
"message": "I've drafted a study that walks new Ledger users back to the moment they hit the bank-connection screen. It opens with how they currently track spending so you get context before the loaded question, then reconstructs the drop-off moment, then probes the trust objection directly — including what a competitor did that felt safe. I kept it to four sections so it stays around 10 minutes.",
"study": {
"id": "stdy_Wc5tJ3hRnB8vQd",
"account_id": "acct_R4vN8sPqKdT2yE",
"display_name": "Why new users stall at bank connection",
"internal_name": "Ledger onboarding — bank connect drop-off",
"objective": "Understand what stops newly signed-up Ledger users from linking a bank account during onboarding, and identify the reassurances that would get them to complete it.",
"context": null,
"welcome_text": "Thanks for joining. We'll spend about 10 minutes on how you keep track of your money and on the first few screens of an app you recently signed up for. There are no right or wrong answers — we're after your honest reactions.",
"questions": "[omitted — use the corresponding get tool to read full data]",
"plan": "[omitted — use the corresponding get tool to read full data]",
"plan_source": "study.plan",
"authoring_method": "ai_chat",
"show_welcome_page": true,
"target_length_min": 5,
"written_in_language": "en",
"template_id": null,
"brand_words": null,
"brand_words_generated": null,
"brand_logo_url": null,
"screen_out_criteria": null,
"enable_tts_default": false,
"tts_voice_id": null,
"uniqueness_constraint": null,
"is_archived": false,
"is_pre_draft": false,
"last_edited_source": null,
"demo_report": null,
"tags": [],
"source": "mcp",
"created_by": "9c2f1e84-6d3b-4a57-b0e2-71f5c8ad4e10",
"created_at": "2026-07-29T11:04:52.117+00:00",
"updated_at": null,
"url": "https://app.voicepanel.com/accounts/acct_R4vN8sPqKdT2yE/studies/stdy_Wc5tJ3hRnB8vQd/create"
}
}description is required; context and written_in_language are optional (language defaults to "en"). The generated plan is written to the study but stripped from this response — plan and questions come back as the literal placeholder string, so call get_study with the returned id to read the actual sections. context is always null on the returned row: the context argument is folded into the AI prompt, not stored on the study. source is always "mcp" for studies created through this server. study is the full studies row, so additional columns beyond those shown here (e.g. authoring_method, set to "ai_chat") are present; the output schema is deliberately loose. This call runs a synchronous LLM generation and takes tens of seconds.
Edit an existing study. If you are writing the plan yourself, call get_study_design_guide first — it covers the plan fields, the research rules this account applies, and the constraints that fail validation. Studies are also called "projects".
Parameters
study_id
string
required
The study ID to edit
display_name
string
New display name
objective
string
New research objective
context
string
New context
questions
object[]
Updated questions, in the legacy flat question format. Prefer `plan` for studies that use sections and steps.
plan
object
Updated study plan. It replaces the whole plan; a plan without `device` keeps the study's current device.
target_length_min
number
New target interview length in minutes
welcome_text
string
New welcome text
written_in_language
string
New language code
internal_name
string
New internal name
show_welcome_page
boolean
Whether to show the welcome page to participants
tag
string
Set the study's label (a study has a single label; pass null or an empty string to remove it). See list_tags.
brand_words
string[]
Brand names and special terms to improve transcription accuracy
enable_tts_default
boolean
Read questions aloud to participants by default (text-to-speech)
tts_voice_id
string
Voice ID used for text-to-speech read-aloud. Options: Sarah (American, female) = EXAVITQu4vr4xnSDxMaL; Nora (Danish, female) = ONFS8Q3TuiPLQCXXa4dy; Lily (British, female) = pFZP5JQG7iQjIQuC4Bku; Katie (Irish, female) = sgk995upfe3tYLvoGcBN; Charlie (Australian, male) = IKne3meq5aSn9XLyUdCD; Daniel (British, male) = onwK4e9ZLuTAKqWW03F9; Adam (American, male) = pNInz6obpgDQGcFmaJgB; Michael (German, male) = 42I57fNB7wi2PIYWo0Xr.
EXAVITQu4vr4xnSDxMaL · ONFS8Q3TuiPLQCXXa4dy · pFZP5JQG7iQjIQuC4Bku · sgk995upfe3tYLvoGcBN · IKne3meq5aSn9XLyUdCD · onwK4e9ZLuTAKqWW03F9 · pNInz6obpgDQGcFmaJgB · 42I57fNB7wi2PIYWo0Xr
uniqueness_constraint
string
Restrict submissions by requiring a custom participant ID
custom_id_required · custom_id_required_single_submission
Returns
id
string
url
string
account_id
string
display_name
string
internal_name
string
status
string
objective
string
context
string
questions
any
plan
any
plan_source
string
welcome_text
string
show_welcome_page
boolean
target_length_min
number
written_in_language
string
template_id
string
brand_words
any
enable_tts_default
boolean
tts_voice_id
string
uniqueness_constraint
string
tags
any
source
string
created_at
string
updated_at
string
Get an AI review of a study's configuration before publishing. Returns a score (1-5) and improvement suggestions. Results are cached per study version. Studies are also called "projects".
Parameters
study_id
string
required
The study ID to review
Returns
id
string
study_version_id
string
reviewer_type
string
data
any
prompt_rules_hash
string
created_at
string
Example
A study is drafted and you want an AI critique of the question design before you spend recruiting budget publishing it.
Arguments
{
"study_id": "stdy_7Kq2mXbA9fLpZv"
}Response
{
"id": "srvw_Qy6bN2vKcR8jTm",
"study_version_id": "stdv_Lp9wF4kTzX2mHc",
"reviewer_type": "ai",
"created_by": "3f8c1d5a-9b24-4e77-8a10-6c2be5417d93",
"data": {
"improvements": [
{
"title": "Section 2, Step 2.1 sets no success criterion for the task",
"description": "The task asks participants to 'book a delivery slot for tomorrow evening, then place the order', but nothing tells them what a finished order looks like in the prototype. Participants will stop at different points, so you won't be able to compare where they gave up — which is the whole point of the study. Say explicitly what the confirmation screen looks like, or that they should stop once they see it.",
"severity": "high",
"exceptionText": "Do not flag the delivery-slot task's missing success criterion; the open stopping point is intentional."
},
{
"title": "The rating scale in Section 2, Step 2.2 has no follow-up",
"description": "A 1-5 ease rating right after the task gives you a number with no explanation. Everyone who rates it a 2 will have a different reason, and those reasons are what would tell you how to fix the slot picker. Add an open-ended step after it asking what made it feel that way.",
"severity": "medium",
"exceptionText": "Do not flag the ease-of-use rating for lacking an open-ended follow-up; that is intentional."
},
{
"title": "Section 1, Step 1.2 frequency question uses vague time bands",
"description": "In Section 1, Step 1.2, 'A few times a week' and 'A few times a month' overlap in how people interpret them, and neither anchors to a period you can segment on later. Consider concrete counts, e.g. '3 or more times a week' and '1-3 times a month'.",
"severity": "low",
"exceptionText": "Do not flag the grocery-frequency question's approximate time bands; they match how customers describe habits."
}
]
},
"decision": null,
"prompt_rules_hash": null,
"created_at": "2026-07-28T18:41:12.663+00:00",
"updated_at": "2026-07-28T18:41:12.663+00:00"
}study_id is the only parameter. Despite the tool description mentioning a score, the review payload contains no score — data is exactly { improvements: [...] }, and each improvement has title, description, severity (critical | high | medium | low) and exceptionText (a one-line restatement stored if someone marks the suggestion as 'not an issue'). The response is the saved study_reviews row, keyed to a study *version*: reviewing the same study again without editing it returns the identical cached row, and editing the study mints a new version so the next call re-runs the model. prompt_rules_hash is null unless the account has custom review prompt rules or review exceptions configured; when set it is part of the cache key, so changing those rules invalidates the cached review. Uncached calls invoke GPT-4.1 synchronously and take roughly 10-30 seconds.
Stimulus media
Create a short-lived web link the user can open to upload their own media (images, video, or audio) from their device for use as study stimulus. Returns upload_url — present this link to the user and ask them to open it and upload their files. Once they confirm they are done, call get_upload_session with the returned session_id to retrieve the uploaded files' public URLs, then use those URLs as stimulus in create_study/edit_study. Use this when the user has a local media file to add; media that is already hosted at a public URL (a website, a YouTube link, a Figma prototype, etc.) can be referenced directly in the stimulus without uploading.
Parameters
None — this tool reads from your connected account.
Returns
session_id
string
upload_url
string
status
string
expires_in_seconds
number
Retrieve the media a user uploaded via an upload session (see create_upload_session). Returns the uploaded files as { url, stimulus_type } — use each url as a stimulus in a study plan (create_study/edit_study). Call this after the user confirms they have finished uploading; status stays 'awaiting_upload' until at least one file has been uploaded.
Parameters
session_id
string
required
The upload session ID from create_upload_session
Returns
session_id
string
status
string
assets
object[]
url
string
filename
string
stimulus_type
string
size_bytes
number
Get a short-lived pre-signed URL to upload ONE media file (image/video/audio) directly to Voicepanel storage, for use as study stimulus. This is for agent/coding clients that can run a shell command (e.g. Claude Code); a plain chat client cannot use it — use create_upload_session (a hosted upload page) for that. Returns upload_url (a pre-signed PUT URL) and public_url. To upload, the client must run the following — WITH THE USER'S APPROVAL, since it reads a local file and runs a shell command:
curl -X PUT "<upload_url>" -H "Content-Type: <mime_type>" --data-binary @<local-file-path>
After the PUT succeeds, use public_url as the stimulus url in create_study/edit_study. Handles large files (e.g. video) that inline uploads can't. Accepted types: JPEG, PNG, GIF, WEBP, MP4, MOV, MP3, M4A, WAV, WEBM (SVG and other types are rejected). HTML is accepted only for accounts with the HTML stimulus feature enabled, and is rejected otherwise.
Parameters
filename
string
required
File name including extension, e.g. 'concept-a.png'.
mime_type
string
required
MIME type of the file; must match the file's actual bytes.
image/jpeg · image/png · image/gif · image/webp · video/mp4 · video/quicktime · audio/mpeg · audio/mp4 · audio/wav · audio/webm · text/html
Returns
upload_url
string
public_url
string
storage_path
string
mime_type
string
stimulus_type
string
expires_in_seconds
number
Read the caller-defined attributes stored on a stimulus — the tags used to group and compare stimuli during analysis (e.g. which model or price tier a variant represents). Stimulus IDs come from the plan returned by get_study, where each stimulus carries its id and, once set, its metadata.
Parameters
stimulus_id
string
required
The stimulus ID
Returns
id
string
study_id
string
metadata
object
Tag a stimulus with caller-defined attributes so analysis can group and compare by them later. Pass metadata to set several attributes at once, or key plus value to set a single one — exactly one of the two. By default metadata is merged into what is already there; pass replace: true to drop every attribute it doesn't name. To remove one attribute, pass its key with a null value; to clear them all, pass an empty metadata with replace: true. Stimulus IDs come from the plan returned by get_study.
Parameters
stimulus_id
string
required
The stimulus ID
metadata
object
The attributes to store, e.g. { "cost": "low", "gated": true, "variant": 2 }. Values may be strings, numbers, booleans, or arrays of strings and numbers — a list may not hold booleans. Nested objects are flattened into dotted keys — { "pricing": { "tier": "pro" } } is stored as { "pricing.tier": "pro" }. Null values are dropped.
replace
boolean
Replace the whole set of attributes with `metadata`, removing any it doesn't name. Ignored when setting a single `key`. Defaults to false (merge).
key
string
Set a single attribute under this key, leaving the rest alone.
value
string | number | array | object
The value for `key` — a string, a number, a boolean, or an array of strings and numbers. An object is flattened under the key and replaces anything already nested there. Null removes the key.
Returns
id
string
study_id
string
metadata
object
Distribute
Choose how responses get collected — a link you share, an intercept on your own site, or a panel recruit.
Your own audience
Create a link distribution — a shareable link the user sends directly to their own respondents. To qualify respondents before the study (useful for reaching a niche audience by sharing the link widely and screening), pass screener_questions directly, or pass an audience description to have the screener generated automatically. Only use this when the user has chosen link sharing; if they haven't said how they want to collect responses, ask them to choose between a link, an on-site intercept (create_intercept_distribution), or panel recruiting (create_panel_distribution). Returns share_url, the public link respondents open to take the study, and url, a dashboard link to view the distribution. Note the distribution starts in draft — publish_distribution makes the share_url live.
Parameters
study_id
string
required
The study ID to create a distribution for
name
string
Display name for the distribution
device_targeting
object
Restrict who can respond by device, OS, or browser
screener_questions
object[]
Screener questions to qualify respondents before the study. Respondents who don't qualify are screened out. Call get_screener_design_guide before writing these by hand — screener qualification tags are easy to get subtly wrong, and a mistagged option either lets everyone through or disqualifies real targets.
audience
string
Plain-text description of the audience you want to qualify for (e.g. 'people who play the tuba'). When provided and screener_questions is omitted, a screener is generated from it automatically (same AI the app's screener generator uses).
quota_limit
number
Maximum number of responses to collect
incentive
object
Incentive offered to respondents for completing the study
Returns
id
string
url
string
share_url
string
study_id
string
name
string
type
string
status
string
distribution_provider
string
config
any
incentive
any
quota_limit
number
quota_usage
number
quota_frequency
string
device_targeting
any
screener_questions
any
default_response_language
string
collect_email
boolean
key
string
published_at
string
created_at
string
updated_at
string
study_data
any
segment_quotas
any
targeting_condition
any
Example
A researcher wants a shareable link they can post to their own audience (newsletter, community, in-app email) rather than recruiting from a panel.
Arguments
{
"study_id": "stdy_9TfLcW3vBn6HdQ",
"name": "Newsletter link — churned freelancers",
"quota_limit": 150,
"audience": "Freelancers who send at least 3 client invoices a month and have used dedicated invoicing software in the past year",
"incentive": {
"amount": 25,
"type": "gift_card",
"title": "$25 gift card",
"description": "We'll email your gift card within 2 business days of finishing the interview."
}
}Response
{
"id": "dst_7Kq2mVbXpR4nZa",
"study_id": "stdy_9TfLcW3vBn6HdQ",
"name": "Newsletter link — churned freelancers",
"description": null,
"type": "link",
"status": "draft",
"distribution_provider": null,
"distribution_provider_id": null,
"config": {},
"incentive": {
"amount": 25,
"type": "gift_card",
"title": "$25 gift card",
"description": "We'll email your gift card within 2 business days of finishing the interview."
},
"quota_limit": 150,
"quota_usage": 0,
"quota_frequency": "lifetime",
"traffic_threshold": null,
"device_targeting": { "device": "any", "os": [], "browser": [] },
"screener_questions": [
{
"text": "How many client invoices do you typically send in a month?",
"type": "radio",
"isRequired": true,
"earlyScreenout": true,
"randomizeOptions": false,
"options": [
{ "text": "None", "qualification": "disqualify", "anchor": false },
{ "text": "1–2", "qualification": "disqualify", "anchor": false },
{ "text": "3–9", "qualification": "qualify", "anchor": false },
{ "text": "10 or more", "qualification": "qualify", "anchor": false }
],
"rationale": "Verifies the 3+ invoices per month volume threshold, which nothing else in the link setup can filter on."
},
{
"text": "Which of these have you used to invoice clients in the past 12 months?",
"type": "checkbox",
"isRequired": true,
"earlyScreenout": true,
"randomizeOptions": true,
"options": [
{ "text": "Dedicated invoicing software", "qualification": "must_one_of", "anchor": false },
{ "text": "Accounting software with invoicing built in", "qualification": "must_one_of", "anchor": false },
{ "text": "Spreadsheets or word processor templates only", "qualification": "may_select", "anchor": false },
{ "text": "None of the above", "qualification": "disqualify", "anchor": true }
],
"rationale": "Confirms past-year use of dedicated invoicing software and screens out spreadsheet-only invoicers."
}
],
"default_response_language": null,
"collect_email": false,
"screen_email": false,
"email_list_id": null,
"automatic_invites": true,
"key": null,
"is_archived": false,
"seq_num": 3,
"complete_redirect_url": null,
"screen_out_redirect_url": null,
"quota_full_redirect_url": null,
"quality_terminate_redirect_url": null,
"published_at": null,
"last_completed_at": null,
"created_by": "0f9b6d1a-3c47-4f5e-8a92-6d0b1e77c4aa",
"created_at": "2026-07-29T16:42:11.238471+00:00",
"updated_at": null,
"study_data": "[omitted — use the corresponding get tool to read full data]",
"segment_quotas": null,
"targeting_condition": null,
"pretranslations": [],
"url": "https://app.voicepanel.com/accounts/acct_2XmNr8QpVt5LKe/studies/stdy_9TfLcW3vBn6HdQ/distribute?distribution_id=dst_7Kq2mVbXpR4nZa",
"share_url": "https://app.voicepanel.com/d/7Kq2mVbXpR4nZa"
}The handler inserts via createDistribution, which selects *, pretranslations:distribution_translations(locale), so the whole distributions row comes back plus pretranslations. stripHeavyFields then replaces DISTRIBUTION_HEAVY_FIELDS (study_data, segment_quotas, targeting_condition) — but only where the value is non-null, so study_data (column default '{}') becomes the placeholder string while the two nullable columns stay null. distributionResult finally attaches url (dashboard deep link) and, because type === "link", share_url (getDistributionLink strips the dst_ prefix → /d/<suffix>); intercept and panel results have no share_url. screener_questions here was generated from audience by generateScreener(audience, []) — passing screener_questions explicitly skips generation, and passing neither stores []. New rows always come back status: "draft" with quota_usage: 0, published_at: null, and updated_at: null (the handle_updated_at trigger only fires on UPDATE).
Create an intercept distribution — invites visitors of the user's own web application via the embedded Voicepanel snippet (shown as 'Intercept' in the Voicepanel app; stored as type 'website'). To qualify visitors before the study, pass screener_questions directly, or pass an audience description to have the screener generated automatically. Only use this when the user has chosen an on-site intercept; if they haven't said how they want to collect responses, ask them to choose between a link (create_link_distribution), an intercept, or panel recruiting (create_panel_distribution). Returns url, a dashboard link to view the distribution — share it with the user.
Parameters
study_id
string
required
The study ID to create a distribution for
name
string
Display name for the distribution
device_targeting
object
Restrict who can respond by device, OS, or browser
screener_questions
object[]
Screener questions to qualify respondents before the study. Respondents who don't qualify are screened out. Call get_screener_design_guide before writing these by hand — screener qualification tags are easy to get subtly wrong, and a mistagged option either lets everyone through or disqualifies real targets.
audience
string
Plain-text description of the audience you want to qualify for (e.g. 'people who play the tuba'). When provided and screener_questions is omitted, a screener is generated from it automatically (same AI the app's screener generator uses).
quota_limit
number
Maximum number of responses to collect
Returns
id
string
url
string
share_url
string
study_id
string
name
string
type
string
status
string
distribution_provider
string
config
any
incentive
any
quota_limit
number
quota_usage
number
quota_frequency
string
device_targeting
any
screener_questions
any
default_response_language
string
collect_email
boolean
key
string
published_at
string
created_at
string
updated_at
string
study_data
any
segment_quotas
any
targeting_condition
any
Panel recruiting
Given a plain-text description of the target audience, recommend which research panel to recruit from, with a rationale and ranked alternatives. Call this before create_panel_distribution to decide the provider — then present the recommendation to the user and let them override. Returns recommended_provider (the best-fit panel this account can use — 'user_interviews' or 'respondent'), rationale, best_fit_label (the ideal panel, which may be a specialty we don't self-serve), requires_contact (true when the ideal fit isn't self-serve), and options (accessible panels ranked best-fit first).
Parameters
description
string
required
Plain-text description of the target audience, e.g. 'French women who play the tuba, living in South America' or 'US enterprise IT admins who manage Okta'
Returns
recommended_provider
string
best_fit_label
string
rationale
string
requires_contact
boolean
options
object[]
provider
string
rationale
string
Example
Before calling create_panel_distribution, to decide which research panel fits a described audience and to show the user ranked alternatives they can override.
Arguments
{
"description": "US-based IT administrators at companies with 200+ employees who manage single sign-on (Okta or Entra ID) day to day"
}Response
{
"recommended_provider": "user_interviews",
"best_fit_label": "Terac",
"rationale": "Verifies professional identity for senior IT and security roles",
"requires_contact": false,
"options": [
{
"provider": "user_interviews",
"rationale": "Reaches vetted IT decision-makers at mid-size US companies"
},
{
"provider": "respondent",
"rationale": "Offers broad US professional coverage as a fallback"
}
]
}Read-only — the handler authenticates and then makes a pure LLM call to generateBestFit, hard-coding availability to { user_interviews: true, respondent: true, prolific: false, terac: false } (Prolific and Terac are behind client-side feature flags the stateless MCP server can't evaluate). That is why best_fit_label can name a panel that isn't in options: here the ranker's top bucket was Terac, which MCP can't self-serve, so recommended_provider falls back to the best-ranked accessible panel and rationale is still the top bucket's rationale. options is result.providers mapped down to just provider and rationale (the internal ranking index is dropped) and only ever contains user_interviews and/or respondent, ranked best-fit first. requires_contact is true — with recommended_provider: null — only when the ideal fit is a contact-only specialty bucket (Emporia, Offerwise, or "other"), e.g. a South America–focused audience.
Create a panel distribution — participants are recruited automatically from a research panel. Created in draft status only; this tool does not contact the panel provider. Publishing a panel recruit is not available over MCP — tell the user to publish it from the Voicepanel dashboard, which is where credits are spent and recruiting starts. Only use this when the user has chosen panel recruiting; if they haven't said how they want to collect responses, ask them to choose between a link (create_link_distribution), an on-site intercept (create_intercept_distribution), or a panel. Call suggest_panel_provider first to pick the provider. Pass an audience description to have targeting filters and a screener generated automatically (required for 'respondent'). Returns url, a dashboard link to view the distribution — share it with the user.
Parameters
study_id
string
required
The study ID to create a distribution for
name
string
Display name for the distribution
device_targeting
object
Restrict who can respond by device, OS, or browser
screener_questions
object[]
Screener questions to filter participants. Overrides any screener generated from `audience`. Call get_screener_design_guide before writing these by hand — gaming risk is real on a panel recruit, and a mistagged option either lets everyone through or disqualifies real targets.
audience
string
Plain-text description of the target audience. When provided, panel targeting filters and a screener are generated from it automatically (same AI the app's 'Create with AI' uses). Required for the 'respondent' provider; optional for 'user_interviews' (where raw targeting_criteria can be passed instead).
provider
string
Which research panel to recruit from: 'user_interviews' (default) or 'respondent'. Call suggest_panel_provider with the audience to get a recommendation, then pass the user's choice here.
user_interviews · respondent
num_participants
number
required
Number of participants to recruit
incentive_amount
number
required
Incentive amount in USD per participant (minimum $5)
title
string
Public-facing title shown to potential panel participants (defaults to the AI-generated title or study name)
description
string
Public-facing description shown to potential panel participants (defaults to the AI-generated description or study objective)
target_market_type
string
Target market type: 'b2c' (consumer, default) or 'b2b' (business)
b2c · b2b
targeting_criteria
object
User Interviews only: raw demographic/professional targeting criteria, as an alternative to `audience`. Example: { 'single_select--country': ['US'], 'single_select--gender': ['woman'] }
Returns
id
string
url
string
share_url
string
study_id
string
name
string
type
string
status
string
distribution_provider
string
config
any
incentive
any
quota_limit
number
quota_usage
number
quota_frequency
string
device_targeting
any
screener_questions
any
default_response_language
string
collect_email
boolean
key
string
published_at
string
created_at
string
updated_at
string
study_data
any
segment_quotas
any
targeting_condition
any
Managing distributions
List all distributions for a study
Parameters
study_id
string
required
The study ID
Returns
distributions
object[]
id
string
url
string
share_url
string
study_id
string
name
string
type
string
status
string
distribution_provider
string
quota_limit
number
quota_usage
number
key
string
created_at
string
Get detailed distribution information including config, incentive, quota, and targeting. Returns url, a dashboard link to view the distribution, and — for link distributions — share_url, the public link participants open to take the study (share this one with respondents).
Parameters
distribution_id
string
required
The distribution ID
Returns
id
string
url
string
share_url
string
study_id
string
name
string
type
string
status
string
distribution_provider
string
config
any
incentive
any
quota_limit
number
quota_usage
number
quota_frequency
string
device_targeting
any
screener_questions
any
default_response_language
string
collect_email
boolean
key
string
published_at
string
created_at
string
updated_at
string
study_data
any
segment_quotas
any
targeting_condition
any
distribution_translations
any
pretranslations
any
Example
To inspect a distribution's current state — quota progress, screener, incentive, publish status — or to re-fetch the participant-facing link for a study you already set up.
Arguments
{
"distribution_id": "dst_7Kq2mVbXpR4nZa"
}Response
{
"id": "dst_7Kq2mVbXpR4nZa",
"study_id": "stdy_9TfLcW3vBn6HdQ",
"name": "Newsletter link — churned freelancers",
"type": "link",
"status": "published",
"distribution_provider": null,
"config": {},
"incentive": {
"amount": 25,
"type": "promo_code",
"code": "FREELANCE25",
"title": "$25 promo code",
"description": "You'll get a $25 promo code as soon as you finish the interview."
},
"quota_limit": 150,
"quota_usage": 87,
"quota_frequency": "lifetime",
"device_targeting": {
"device": "any",
"browser": [],
"os": []
},
"screener_questions": [
{
"text": "How many client invoices do you typically send in a month?",
"type": "radio",
"isRequired": true,
"earlyScreenout": true,
"randomizeOptions": false,
"options": [
{ "text": "None", "qualification": "disqualify", "anchor": false },
{ "text": "1–2", "qualification": "disqualify", "anchor": false },
{ "text": "3–9", "qualification": "qualify", "anchor": false },
{ "text": "10 or more", "qualification": "qualify", "anchor": false }
]
}
],
"default_response_language": "en",
"collect_email": false,
"key": null,
"published_at": "2026-07-29T17:05:44.512Z",
"created_at": "2026-07-29T16:42:11.238Z",
"updated_at": "2026-07-29T17:05:44.512Z",
"url": "https://app.voicepanel.com/accounts/acct_2XmNr8QpVt5LKe/studies/stdy_9TfLcW3vBn6HdQ/distribute?distribution_id=dst_7Kq2mVbXpR4nZa",
"share_url": "https://app.voicepanel.com/d/7Kq2mVbXpR4nZa"
}This tool goes through fetchDistribution, which passes an explicit column projection to getDistribution — so the payload is exactly those columns plus the computed url/share_url, and nothing else. Two consequences worth documenting: (1) study_data, segment_quotas, and targeting_condition are simply absent rather than replaced with the strip placeholder — distributionResult is called with strip: false here precisely because the projection already excludes them; (2) pretranslations / distribution_translations are also absent, since the default getDistribution select that would join them is overridden. share_url appears only for type: "link". For an intercept (type: "website") there is no share_url and key holds the snippet identifier passed to Voicepanel.showSurvey(key); for a panel, distribution_provider is "user_interviews" or "respondent" and config holds the provider payload (public title, participant count, incentive amount, targeting criteria).
Edit an existing distribution. Panel distributions can only be edited while in draft status.
Parameters
distribution_id
string
required
The distribution ID to edit
name
string
New display name
quota_limit
number
New maximum number of responses (for link/website types)
incentive
object
New incentive configuration (for link distributions)
device_targeting
object
New device, OS, and browser restrictions
screener_questions
object[]
Updated screener questions. Call get_screener_design_guide before writing these by hand.
default_response_language
string
Default language for responses
collect_email
boolean
Whether to collect participant email
num_participants
number
New number of participants to recruit (panel only)
incentive_amount
number
New incentive amount in USD per participant (panel only)
title
string
New public-facing title (panel only)
description
string
New public-facing description (panel only)
target_market_type
string
New target market type (panel only)
b2c · b2b
targeting_criteria
object
New targeting criteria (panel only)
Returns
id
string
url
string
share_url
string
study_id
string
name
string
type
string
status
string
distribution_provider
string
config
any
incentive
any
quota_limit
number
quota_usage
number
quota_frequency
string
device_targeting
any
screener_questions
any
default_response_language
string
collect_email
boolean
key
string
published_at
string
created_at
string
updated_at
string
study_data
any
segment_quotas
any
targeting_condition
any
Publish a distribution to make it live and start collecting responses. For link distributions, returns share_url — the public link to share with respondents, now live. Supports link and intercept distributions; publishing panel distributions is not currently available over MCP (publish those from the Voicepanel dashboard).
Parameters
distribution_id
string
required
The distribution ID to publish
Returns
id
string
url
string
share_url
string
study_id
string
name
string
type
string
status
string
distribution_provider
string
config
any
incentive
any
quota_limit
number
quota_usage
number
quota_frequency
string
device_targeting
any
screener_questions
any
default_response_language
string
collect_email
boolean
key
string
published_at
string
created_at
string
updated_at
string
study_data
any
segment_quotas
any
targeting_condition
any
Analyze
Read what participants said, dig into the AI findings, and slice the results by segment.
Responses
Get response statistics for a study (counts by status and distribution). Studies are also called "projects".
Parameters
study_id
string
required
The study ID
Returns
An open object — the shape depends on the study.
List responses for a study (excludes full conversation transcripts — use get_response for that)
Parameters
study_id
string
required
The study ID
status
string
Filter by response status (e.g. 'completed', 'incomplete')
limit
number
Max number of responses to return (default 50, max 200)
Returns
responses
object[]
id
string
status
string
status_reason
string
created_at
string
summary
any
format
string
seconds_elapsed
number
quality_score
any
language
string
string
mode
string
seq_num
number
Example
Reach for this when you want to scan who has taken a study — how many sessions came in, how long they ran, how they scored on quality — and pick out the response IDs worth reading in full.
Arguments
{
"study_id": "stdy_7Qa2mZ4vXpKr9d",
"status": "completed",
"limit": 3
}Response
{
"responses": [
{
"id": "9c3f5b41-7a2e-4d18-b6c0-1e8f24a5d093",
"status": "completed",
"status_reason": "answered_all_questions",
"created_at": "2026-07-24T15:12:47.913+00:00",
"summary": "Uses Smart Cart weekly for staples but re-checks every line because substitutions replace brands she cares about.",
"format": "audio",
"seconds_elapsed": 431,
"quality_score": {
"overallRating": "high",
"reason": "Concrete, first-hand detail about a recent order; answers stay on the substitution topic.",
"facets": {
"isAutomated": "no",
"isTester": "no",
"isRelevantToObjective": "yes",
"isOnTopic": "yes",
"isContradictory": "no",
"isDeep": "yes",
"isCoherent": "yes",
"isSpecific": "yes",
"isTimingSuspicious": "no"
},
"version": "2025-02-27"
},
"language": "en-US",
"email": "dana.whitfield@example.com",
"mode": "production",
"seq_num": 48
},
{
"id": "1b7d0ae2-4c93-4f60-8a15-72c6e9d4b118",
"status": "completed",
"status_reason": "answered_all_questions",
"created_at": "2026-07-24T13:58:02.406+00:00",
"summary": "Abandoned Smart Cart after two tries; says rebuilding the list by hand is faster than fixing it.",
"format": "chat",
"seconds_elapsed": 268,
"quality_score": {
"overallRating": "medium",
"reason": "On topic but short answers with little elaboration when probed.",
"facets": {
"isAutomated": "no",
"isTester": "no",
"isRelevantToObjective": "yes",
"isOnTopic": "yes",
"isContradictory": "no",
"isDeep": "no",
"isCoherent": "yes",
"isSpecific": "neutral",
"isTimingSuspicious": "no"
},
"version": "2025-02-27"
},
"language": "en-US",
"email": null,
"mode": "production",
"seq_num": 47
},
{
"id": "4e60c8fa-91b2-4a77-bf39-05dd1c7e2a64",
"status": "completed",
"status_reason": "answered_all_questions",
"created_at": "2026-07-23T21:04:33.771+00:00",
"summary": "Relies on Smart Cart for a fixed weekly staples run and trusts substitutions for pantry items but not produce.",
"format": "audio",
"seconds_elapsed": 512,
"quality_score": {
"overallRating": "very-high",
"reason": "Long, specific answers with clear reasoning about when substitutions are acceptable.",
"facets": {
"isAutomated": "no",
"isTester": "no",
"isRelevantToObjective": "yes",
"isOnTopic": "yes",
"isContradictory": "no",
"isDeep": "yes",
"isCoherent": "yes",
"isSpecific": "yes",
"isTimingSuspicious": "no"
},
"version": "2025-02-27"
},
"language": "en-US",
"email": "m.okafor@example.com",
"mode": "production",
"seq_num": 46
}
]
}The array is always wrapped under the responses key. Rows are the light field set only — messages (the transcript), segments, screener_responses and even study_id are not returned here; use get_response for those. Results are ordered by seq_num descending (newest participant first) and limit is clamped to 200 (default 50). quality_score is a JSONB blob, never a number, and is null on responses that have not been rated yet; summary is likewise null until the post-interview summarization job runs, which is typical of incomplete responses when you omit the status filter. If the serialized payload exceeds the 200K-char cap the array is halved and the payload gains _truncated: true, _totalItems and _returnedItems alongside responses.
Get a single response with full conversation transcript
Parameters
response_id
string
required
The response ID
Returns
id
string
study_id
string
status
string
status_reason
string
created_at
string
messages
any
summary
any
segments
any
screener_responses
any
quality_score
any
format
string
seconds_elapsed
number
language
string
string
mode
string
feedback
any
seq_num
number
data
string
Example
Reach for this when you need one participant's full verbatim transcript — after spotting the response in `list_responses` or in the evidence attached to an analysis insight.
Arguments
{
"response_id": "9c3f5b41-7a2e-4d18-b6c0-1e8f24a5d093"
}Response
{
"id": "9c3f5b41-7a2e-4d18-b6c0-1e8f24a5d093",
"study_id": "stdy_7Qa2mZ4vXpKr9d",
"status": "completed",
"status_reason": "answered_all_questions",
"created_at": "2026-07-24T15:12:47.913+00:00",
"messages": [
{
"role": "system",
"content": "Thanks for joining. To start — walk me through the last time you used Smart Cart to build a grocery order.",
"questionNumber": 1,
"probeNumber": 0,
"sectionNumber": 1,
"stepNumber": 1,
"secondsElapsed": 4
},
{
"role": "user",
"content": "Sunday night. I tapped Smart Cart and it pulled in about twenty things from my usual list, which was great, but it swapped my oat milk for a store brand and put in the wrong size of coffee. So I went through the whole cart line by line anyway.",
"questionNumber": 1,
"probeNumber": 0,
"sectionNumber": 1,
"stepNumber": 1,
"secondsElapsed": 47
},
{
"role": "system",
"content": "That line-by-line pass sounds tedious — roughly how long did it take you?",
"questionNumber": 1,
"probeNumber": 1,
"sectionNumber": 1,
"stepNumber": 1,
"secondsElapsed": 53
},
{
"role": "user",
"content": "Ten minutes, maybe more, because I had to open each item to see what it actually was. That's about what it would have taken me to build the cart from scratch, so I'm not really saving anything.",
"questionNumber": 1,
"probeNumber": 1,
"sectionNumber": 1,
"stepNumber": 1,
"secondsElapsed": 96
},
{
"role": "system",
"content": "On a scale of 1 to 5, how likely are you to keep using Smart Cart for your weekly order?",
"questionNumber": 2,
"probeNumber": 0,
"sectionNumber": 1,
"stepNumber": 2,
"secondsElapsed": 312
},
{
"role": "user",
"content": "<Respondent selected \"3\">",
"structuredContent": {
"type": "rating_scale",
"content": "3"
},
"questionNumber": 2,
"probeNumber": 0,
"sectionNumber": 1,
"stepNumber": 2,
"secondsElapsed": 349
}
],
"summary": "Uses Smart Cart weekly for staples but re-checks every line because substitutions replace brands she cares about.",
"segments": {
"age": {
"value": "30-39",
"error": null
},
"order_frequency": {
"value": "weekly",
"error": null
}
},
"screener_responses": [
{
"type": "radio",
"questionText": "How often do you order groceries for delivery or pickup?",
"answers": [
{
"text": "Once a week or more"
}
]
},
{
"type": "checkbox",
"questionText": "Which of these have you used in the past month?",
"answers": [
{
"text": "A grocery chain's own app"
},
{
"text": "A third-party delivery marketplace"
}
]
}
],
"quality_score": {
"overallRating": "high",
"reason": "Concrete, first-hand detail about a recent order; answers stay on the substitution topic.",
"facets": {
"isAutomated": "no",
"isTester": "no",
"isRelevantToObjective": "yes",
"isOnTopic": "yes",
"isContradictory": "no",
"isDeep": "yes",
"isCoherent": "yes",
"isSpecific": "yes",
"isTimingSuspicious": "no"
},
"version": "2025-02-27"
},
"format": "audio",
"seconds_elapsed": 431,
"language": "en-US",
"email": "dana.whitfield@example.com",
"mode": "production",
"feedback": {
"vote": "up"
},
"seq_num": 48
}The payload is the response row itself — not wrapped under a key. messages is the full transcript; the excerpt above is shortened for the docs, a real 7-minute session runs to dozens of turns. role is system for the AI moderator and user for the participant; probeNumber is 0 for the scripted question and increments for each follow-up probe. structuredContent appears only on answers to structured question types (rating_scale, single_select, multi_select, ranking, matrix, task). segments is keyed by segmentation key, each { value, error }. feedback is the optional end-screen thumbs vote and is null for most responses; email is null unless the distribution collects it. In the rare case the row exceeds the 200K-char cap the whole payload is replaced by { "_truncated": true, "data": "<serialized prefix>" }.
Update a response's status and/or whether it is included in analysis
Parameters
response_id
string
required
The response ID
status
string
New status for the response (e.g. 'completed', 'incomplete', 'screen_out')
status_reason
string
Reason for the status change
include_in_analysis
boolean
Whether to include this response in analysis
Returns
response_id
string
updated
object
status
string
status_reason
string
include_in_analysis
boolean
Findings
Get AI-generated analysis insights (the findings/summary) for a study — the latest completed summary, evidence omitted. Start here for high-level findings. Use get_analysis_insight for full evidence quotes. The returned _versions array lists prior summary versions you can fetch by id. Pass analyze_in_original_language to view the summary in the study's original language instead of English.
Parameters
study_id
string
required
The study ID
analyze_in_original_language
boolean
View the summary in the study's original language instead of English (default false). Only dual-language studies have an original-language variant; otherwise falls back to whatever exists.
Returns
id
string
analysis_run_id
string
study_id
string
analyze_in_original_language
boolean
summary
any
created_at
string
data
string
Example
You want the headline findings for a study — the latest AI-generated executive summary plus the top insights per section — before drilling into individual quotes.
Arguments
{
"study_id": "stdy_G3nH8kPqR2Wm5x",
"analyze_in_original_language": false
}Response
{
"id": "ai_7Kd2mQx9RbTv4L",
"created_at": "2026-07-21T16:42:08.311427+00:00",
"study_id": "stdy_G3nH8kPqR2Wm5x",
"objective": "Understand why new Hearth users abandon setup before generating their first weekly meal plan.",
"summary": {
"allInsights": [],
"topInsights": [],
"researchSummary": "**Research goal:** Understand why new Hearth users abandon setup before generating their first weekly meal plan, and what would carry them through to a plan they trust.\n\n**Research participants:** Eleven U.S. participants (ages 26-48) who installed Hearth in the last 30 days and stopped somewhere in setup; eight cook dinner at home four or more nights a week, and six had previously tried at least one other meal-planning app.\n\n**Key Takeaway:** The promise of \"a week of dinners planned for me\" lands within seconds, but the eleven-question intake is read as paperwork rather than personalization, and with no step counter participants assumed it could keep going (7/11 named length or uncertainty as the reason they quit). The 6 participants who reached the plan preview responded warmly to the recipe cards, yet without a grocery total the plan read as inspiration rather than something to shop from - showing progress during intake and a cost estimate at preview are the two highest-leverage fixes.",
"sectionInsights": [
{
"sectionId": "1",
"allInsights": [
{
"insight": {
"name": "Setup reads as paperwork",
"description": "Participants described the dietary intake as a form to get through rather than a step that would make their meal plan better."
},
"evidence": [
{
"quote": "It felt like filling out an intake form at a doctor's office. I just wanted to see the dinners.",
"media_id": "0b3f9c41-6d2e-4a77-9c15-2f8ad4e1b3aa",
"media_type": "audio",
"responseId": "c41a7d92-58b6-4f03-9a2e-1d7c65b0e884",
"end_timestamp": 14.62,
"start_timestamp": 3.18
},
{
"quote": "None of those questions felt like they were for me. They felt like they were for the app.",
"media_id": "7e52aa08-9114-4bd6-8c3f-45b91d20c6f7",
"media_type": "audio",
"responseId": "9f2c8b17-4ad3-4e59-b0d1-3c6ea7195f42",
"end_timestamp": 21.05,
"start_timestamp": 9.44
},
{
"quote": "I kept thinking, why do you need all this before you show me anything?",
"media_id": null,
"media_type": null,
"responseId": "2d84f6c0-7b19-42ae-8e57-b1f309ac7d63",
"end_timestamp": null,
"start_timestamp": null
}
]
},
{
"insight": {
"name": "No sense of progress",
"description": "Without a step counter, participants assumed the questions could keep going indefinitely and several quit to avoid finding out."
},
"evidence": [
{
"quote": "There was no, like, three of eight. So for all I knew there were twenty more of these.",
"media_id": "b8d61f37-2c40-4b92-a1e6-58c7d0934ef1",
"media_type": "audio",
"responseId": "c41a7d92-58b6-4f03-9a2e-1d7c65b0e884",
"end_timestamp": 18.9,
"start_timestamp": 6.27
},
{
"quote": "I closed it because I didn't want to find out how long it was going to be.",
"media_id": "3a97c5e2-81df-4c06-9b73-6e2418d5a09b",
"media_type": "audio",
"responseId": "5b0e3947-c28a-41d6-83f5-7a91cd402e1b",
"end_timestamp": 11.34,
"start_timestamp": 2.05
}
]
}
],
"topInsights": [
{
"insight": {
"name": "Setup reads as paperwork",
"description": "Participants described the dietary intake as a form to get through rather than a step that would make their meal plan better."
},
"evidenceCount": 3
},
{
"insight": {
"name": "No sense of progress",
"description": "Without a step counter, participants assumed the questions could keep going indefinitely and several quit to avoid finding out."
},
"evidenceCount": 2
}
],
"sectionTitle": "Intake Reads As Paperwork",
"sectionSummary": "Participants understood the value proposition almost immediately, but 8/11 described the dietary intake as a form to get through rather than a step that would improve their plan. Without a step counter, 7/11 said they had no idea how many questions remained and several quit rather than find out. Drop-off clustered at the dietary preferences step and was driven by perceived length rather than any single question."
},
{
"sectionId": "2",
"allInsights": [
{
"insight": {
"name": "Missing cost blocks commitment",
"description": "Participants would not commit to a weekly plan without seeing the grocery total it implied."
},
"evidence": [
{
"quote": "The recipes look great, but I have no idea if this is a sixty dollar week or a two hundred dollar week.",
"media_id": "f19c47b3-0a68-4d51-bb92-8c30e7a6412d",
"media_type": "audio",
"responseId": "7c3d1a58-9e04-4b27-a6f1-20b8e59c3d76",
"end_timestamp": 19.71,
"start_timestamp": 4.88
},
{
"quote": "I'd treat this as ideas, not as my actual shopping list, until it tells me the price.",
"media_id": "6ce802df-45b1-4a39-92c7-d0f31b8e5a24",
"media_type": "audio",
"responseId": "9f2c8b17-4ad3-4e59-b0d1-3c6ea7195f42",
"end_timestamp": 27.4,
"start_timestamp": 12.16
}
]
}
],
"topInsights": [
{
"insight": {
"name": "Missing cost blocks commitment",
"description": "Participants would not commit to a weekly plan without seeing the grocery total it implied."
},
"evidenceCount": 2
}
],
"sectionTitle": "Preview Needs Grocery Cost",
"sectionSummary": "All 6 participants who reached the plan preview reacted warmly to the recipe cards, but 5/6 said the missing grocery total kept them from treating the plan as something they would actually shop from. Participants framed the preview as inspiration rather than a commitment, and 4/6 volunteered that a weekly cost estimate would be the deciding factor."
}
]
},
"status": "completed",
"attempt_number": 1,
"created_by": null,
"schema_version": 2,
"analyze_in_original_language": false,
"analysis_run_id": "ar_Vb9TnQ2sLd6Kfy",
"is_frozen": false,
"_versions": [
{
"id": "ai_7Kd2mQx9RbTv4L",
"created_at": "2026-07-21T16:42:08.311427+00:00",
"analyze_in_original_language": false,
"attempt_number": 1,
"status": "completed"
},
{
"id": "ai_3Wq8PnZc5Ykr2H",
"created_at": "2026-07-09T11:05:44.902581+00:00",
"analyze_in_original_language": false,
"attempt_number": 1,
"status": "completed"
},
{
"id": "ai_Lm5xD9vRt4Bqz7",
"created_at": "2026-06-30T09:18:57.14036+00:00",
"analyze_in_original_language": false,
"attempt_number": 2,
"status": "completed"
}
]
}Returns the full analysis_insights row (the handler selects *) with two modifications. Evidence arrays are stripped from summary: each entry under summary.sectionInsights[].topInsights (and summary.topInsights on legacy v1 rows) loses its evidence array and gains an evidenceCount integer — fetch a version with get_analysis_insight to read the verbatim quotes and response IDs. _versions is appended: a lightweight list of prior completed summary versions in the same language (id, created_at, analyze_in_original_language, attempt_number, status), each fetchable by id via get_analysis_insight. summary shape depends on schema_version — v2 rows carry researchSummary (markdown) plus sectionInsights. If the whole payload exceeds 200K characters it is replaced by { "_truncated": true, "data": "<serialized prefix>" }.
Get a full analysis insight with evidence quotes and response IDs
Parameters
insight_id
string
required
The analysis insight ID
Returns
id
string
analysis_run_id
string
study_id
string
summary
any
created_at
string
data
string
Get analysis questions with summaries and answer categories for a study — the latest result per question (continuously maintained, not tied to a run), evidence omitted. Use get_analysis_question for full evidence quotes. Pass analyze_in_original_language to view summaries in the study's original language instead of English.
Parameters
study_id
string
required
The study ID
analyze_in_original_language
boolean
View question summaries in the study's original language instead of English (default false). Affects open-ended/conversation questions only; counts are language-independent.
Returns
questions
object[]
id
string
question
string
question_type
string
status
string
categories
any
summary
any
position
number
Get a full analysis question with evidence quotes and response IDs per category
Parameters
question_id
string
required
The analysis question ID
Returns
id
string
study_id
string
question
string
question_type
string
analyze_in_original_language
boolean
status
string
categories
any
summary
any
position
number
created_at
string
data
string
Example
You have a question ID from `get_analysis_questions` and now need the full evidence behind it — every answer category with its verbatim respondent quotes and the response IDs they came from.
Arguments
{
"question_id": "aq_Rt6xZ4mKp9Hbc2"
}Response
{
"id": "aq_Rt6xZ4mKp9Hbc2",
"study_id": "stdy_G3nH8kPqR2Wm5x",
"question": "Walk me through what was going through your mind on the dietary preferences screen.",
"question_type": "conversation",
"analyze_in_original_language": false,
"position": 4,
"created_at": "2026-06-28T14:02:19.774+00:00",
"analysis_categorizations": [
{
"id": "ac_Ny7Wq3LdF8vRm5",
"summary": {
"categories": [
{
"name": "Too many questions up front",
"description": "Participants who abandoned setup pointed to the **length** of the intake rather than any single question, describing it as work owed before getting anything back.",
"evidence": [
{
"responseId": "9c1f4a3e-7b2d-4c58-a1f0-6e3d90b47a52",
"quote": "I kept tapping next and it **just kept going**, so at some point I put the phone down."
},
{
"responseId": "41ab08d7-3e95-4f22-9c60-b8127de5a4f1",
"quote": "It felt like **filling out an intake form at a doctor's office**...I hadn't seen a single meal yet and I was already on question eight."
}
]
},
{
"name": "Unsure how answers would be used",
"description": "Several participants answered the allergy and cuisine questions but **could not say what the app would do with them**, which made the effort feel unjustified.",
"evidence": [
{
"responseId": "7d20e6b4-5a11-49c8-8f37-2c9be0143d6a",
"quote": "I said no shellfish, but I **had no idea if that removes recipes or just flags them**."
}
]
},
{
"name": "Wanted to browse before committing",
"description": "A smaller group wanted to **see the recipe library first** and treat the preferences as something to fix later.",
"evidence": [
{
"responseId": "b5e3c81f-9d47-4a06-bb52-10f7a8c2e934",
"quote": "Let me **look around for two minutes first**, then ask me what I don't eat."
}
]
}
],
"stats": {
"totalCategorizedResponses": 4,
"totalUncategorizedResponses": 0
}
},
"categories": null,
"status": "finished_analyzing",
"attempt_number": 1,
"is_default": true,
"is_frozen": false,
"label": null
}
]
}The payload is the analysis_questions row restricted to the columns the handler selects — id, study_id, question, question_type, analyze_in_original_language, position, created_at — plus an analysis_categorizations array holding the latest default categorization (zero or one element; empty array if none exists yet). Note that status, categories, and summary live inside that nested object, not at the top level, even though the loose output schema also permits them there. Unlike get_analysis_questions, nothing is stripped: summary.categories[].evidence carries the verbatim quotes (markdown **bold** marks the justifying span) alongside the responseId each came from, so quotes can be traced back with get_response. If the payload exceeds 200K characters it is replaced by { "_truncated": true, "data": "<serialized prefix>" }.
Generate a fresh analysis summary (findings/insights) for a study from its responses — the same action as the Update button on the analyze page. This does NOT regenerate per-question analysis (that is maintained automatically as responses complete); it produces a new summary version asynchronously. Requires at least one analysis question to have finished processing.
Parameters
study_id
string
required
The study ID to analyze
Returns
message
string
analysisRunId
string
responseCount
number
Segments
List segmentations for a study (name, key, description). Use get_segmentation for full segment definitions.
Parameters
study_id
string
required
The study ID
Returns
segmentations
object[]
id
string
study_id
string
name
string
key
string
description
string
is_read_only
boolean
created_at
string
updated_at
string
Example
You want to see how a study's responses are already sliced (age, plan tier, recruitment channel) before asking for segment-level analysis or creating a new segmentation.
Arguments
{
"study_id": "stdy_67WkmVS85LuT3M"
}Response
{
"segmentations": [
{
"id": "sgmt_H3wTyu8QcZmR1p",
"study_id": "stdy_67WkmVS85LuT3M",
"name": "Age",
"key": "age",
"description": null,
"is_read_only": true,
"created_at": "2026-06-12T09:31:44.207183+00:00",
"updated_at": null
},
{
"id": "sgmt_Xb7pQnR2vKm4Ls",
"study_id": "stdy_67WkmVS85LuT3M",
"name": "Plan tier",
"key": "plan_tier",
"description": "Free vs. Premium subscribers at the time of the interview",
"is_read_only": false,
"created_at": "2026-06-18T15:42:09.118201+00:00",
"updated_at": "2026-07-02T11:07:51.664213+00:00"
},
{
"id": "sgmt_Lq5vD8nWyH2tCf",
"study_id": "stdy_67WkmVS85LuT3M",
"name": "Recruitment channel",
"key": "recruitment_channel",
"description": "Which distribution the participant came from",
"is_read_only": false,
"created_at": "2026-06-18T15:44:02.883410+00:00",
"updated_at": null
}
]
}study_id is the only input parameter. The handler calls fetchSegmentations, which selects exactly these eight columns — segments (the matching criteria) is deliberately not returned; call get_segmentation for that. Rows come back ordered by name ascending. listResult("segmentations", data) wraps the array under the segmentations key; the sibling keys _truncated, _totalItems, and _returnedItems are added only when the serialized payload exceeds the 200,000-char cap, which a segmentation list realistically never hits. An empty study returns { "segmentations": [] }.
Get full segmentation details including segment definitions and matching criteria
Parameters
segmentation_id
string
required
The segmentation ID
Returns
id
string
study_id
string
name
string
key
string
description
string
is_read_only
boolean
segments
array
created_at
string
updated_at
string
Create a new segmentation to categorize responses for a study
Parameters
study_id
string
required
The study ID
name
string
required
Display name for the segmentation
key
string
required
Unique key identifier for the segmentation
description
string
Description of the segmentation
segments
object[]
required
Segment definitions. Every segment needs all four fields — the database rejects the insert otherwise.
Returns
id
string
study_id
string
name
string
key
string
description
string
is_read_only
boolean
segments
array
created_at
string
updated_at
string
Example
You want to split a study's responses into named groups — e.g. participants who had already set up a savings goal versus those who hadn't — so later analysis can be compared across segments.
Arguments
{
"study_id": "stdy_67WkmVS85LuT3M",
"name": "Savings goal usage",
"key": "goal_usage",
"description": "Whether the participant had set up a savings goal before the interview",
"segments": [
{
"name": "Has a savings goal",
"value": "has_goal",
"definition": {
"type": "group",
"operator": "and",
"conditions": [
{
"target": {
"type": "screener",
"question": "Have you set up a savings goal in the app?"
},
"matcher": {
"operator": "equals",
"value": "Yes"
}
}
]
},
"definition_overrides": {}
},
{
"name": "No savings goal",
"value": "no_goal",
"definition": {
"type": "group",
"operator": "and",
"conditions": [
{
"target": {
"type": "screener",
"question": "Have you set up a savings goal in the app?"
},
"matcher": {
"operator": "equals",
"value": "No"
}
}
]
},
"definition_overrides": {}
}
]
}Response
{
"id": "sgmt_4mKpQ9rZbT1xVd",
"study_id": "stdy_67WkmVS85LuT3M",
"name": "Savings goal usage",
"key": "goal_usage",
"description": "Whether the participant had set up a savings goal before the interview",
"segments": [
{
"name": "Has a savings goal",
"value": "has_goal",
"definition": {
"type": "group",
"operator": "and",
"conditions": [
{
"target": {
"type": "screener",
"question": "Have you set up a savings goal in the app?"
},
"matcher": {
"operator": "equals",
"value": "Yes"
}
}
]
},
"definition_overrides": {}
},
{
"name": "No savings goal",
"value": "no_goal",
"definition": {
"type": "group",
"operator": "and",
"conditions": [
{
"target": {
"type": "screener",
"question": "Have you set up a savings goal in the app?"
},
"matcher": {
"operator": "equals",
"value": "No"
}
}
]
},
"definition_overrides": {}
}
],
"segment_function": null,
"is_read_only": false,
"created_by": "9d4e0b3c-7a1f-4e52-8c6b-2f0a9e17d5b4",
"created_at": "2026-07-29T18:04:22.517394+00:00",
"updated_at": null
}Inputs are study_id, name, key, segments (required) and description (optional). The handler passes them to createSegmentation in packages/sdk/segmentation-queries.ts, which first checks the study's existing segmentations for a case-insensitive name or key collision and returns an error result (isError: true, no structuredContent) such as A segmentation with the key "goal_usage" already exists for this study. On success it inserts and returns the full row via .select().single(), handed to toolResult(...) unchanged — so the response is the whole segmentations row, including columns the caller never sent: segment_function (null unless set through the REST API), is_read_only (false by default), created_by (populated by the set_created_by trigger from the OAuth user), created_at, and updated_at (null until a later update fires moddatetime). No fields are stripped here, so segments echoes back exactly what was stored. Each segment must satisfy the table's check_segments JSON-schema constraint: name, value, definition, and definition_overrides are all required, definition being a { type: "group", operator: "and" | "or", conditions: [...] } block whose conditions pair a target (screener, demographic, distribution, query_param, step, or response) with a matcher (equals, not_equals, in, not_in, greater_than, less_than, regexp_match, or llm_matches with an llm_prompt). Unlike the web app, the MCP tool skips automatic processing of existing responses — segment evaluation happens on demand.
Example prompts
Once connected, talk to your assistant the way you would talk to a researcher:
- “Generate a study to find out why trial users don't convert, then review it and tell me what's weak.”
- “Set up a link distribution for it, and screen for people who cancelled in the last 90 days.”
- “What are the top themes across last week's responses?”
- “Show me the actual quotes behind that finding.”
- “How did mobile participants answer differently from desktop?”
- “Which panel should I recruit enterprise IT admins from?”
Limits
- Large results are truncated. A single tool result is capped at 200,000 characters. Ask for a filtered or smaller slice if a study has a lot of responses.
- Analysis runs in the background. Starting an analysis returns immediately; read the findings a few minutes later.
- Publishing spends credits. Publishing a distribution starts collecting real responses, so your assistant should always confirm with you first.
- Pausing and resuming happens in the app. Changing a live distribution's status is not exposed over MCP yet.
Troubleshooting
The connector says it is unauthorized
Your session expired. Disconnect and reconnect the connector to sign in again.
A tool is missing or returns forbidden
Write tools are hidden from viewers. Ask an admin on your account to change your role, then reconnect so the tool list refreshes.
I am seeing the wrong studies
The connector is bound to the account you picked on the consent screen. Disconnect, reconnect, and choose the other account.
Claude Code lists Voicepanel as hidden
A server you added with claude mcp add takes precedence over a claude.ai connector pointing at the same URL, and /mcp marks the connector hidden rather than running both. Keep whichever you prefer and remove the other.
My assistant will not stop truncating
Narrow the request — a single study, a status filter, or a smaller batch of responses — rather than asking for everything at once.
Support
Questions, bug reports, or a tool you wish existed — email support@voicepanel.com.