# Create Study Source: https://docs.listenlabs.ai/api-v2/create-study api-v2/openapi.yaml POST /api/public/v1/studies/create Validate a study guide and create a draft study in your organization # Get Single Response Source: https://docs.listenlabs.ai/api-v2/get-response api-v2/openapi.yaml GET /api/public/v1/responses/{linkId}/{responseId} Retrieve one response's transcript, URL parameters, and summaries by UUID or readable ID # Get Study Questions Source: https://docs.listenlabs.ai/api-v2/get-study-questions GET https://listenlabs.ai/api/public/v1/studies/{studyId}/questions Get the participant-facing questions and concepts from a study's latest revision Each question's `id` matches the `discussionGuideQuestionId` field in the [response endpoints](/api-v2/list-responses), so you can join questions with their corresponding answers and transcript rows — see the [Data Map](/data-map). ### Path Parameters The study's `id` or `linkId`, both returned by [List Studies](/api-v2/list-studies). The legacy endpoint accepts only the `linkId`. ### Response The participant-facing questions from the study's latest editable revision. Each question has a `type` field that determines its shape. #### Common Fields All question types share these base fields: Unique identifier for the question. Matches the `discussionGuideQuestionId` field in the response endpoints, enabling you to join questions with their corresponding transcript rows and answers. The question text shown to participants. Whether this question is part of the screening section. The question type. One of: `open_ended`, `file_upload`, `multiple_choice`, `ranking`, `statement`, `matrix`, `max_diff`. The human-readable question number (1-based) for display purposes. An array of concept objects attached to this question (empty if the question is not part of a concept test block). Unique identifier for the concept. Referenced by `conceptId` on answers and transcript rows. The concept title. The concept description. An array of media attachments. The media type. One of: `image`, `video`. The file name of the media. The URL of the media file. An optional embed URL for the concept (e.g. a Figma or prototype link). #### Multiple Choice Questions `type: "multiple_choice"` questions additionally include: Whether the participant can select multiple options. The list of answer options. #### Ranking Questions `type: "ranking"` questions additionally include: The list of items to rank. #### Matrix Questions `type: "matrix"` questions additionally include: Whether multiple selections are allowed per row. The row labels rated by the participant. The column labels each row is rated across. #### MaxDiff Questions `type: "max_diff"` questions additionally include: The items being compared. What participants judge the items on (e.g. "importance"). How many items are shown per comparison screen. #### Open-Ended, File Upload & Statement Questions `type: "open_ended"`, `type: "file_upload"`, and `type: "statement"` questions carry only the common fields. ### Errors Errors share the [common envelope](/api-v2/overview#errors) (`error` + `code`): | Status | Code | Meaning | | ------ | ------------------------------------- | -------------------------------------------------------------- | | `401` | `missing_api_key` / `invalid_api_key` | Missing or invalid API key. | | `403` | `forbidden` | The key's user isn't a member of the key's organization. | | `404` | `study_not_found` | Study not found in the key's organization. | | `500` | `internal_error` | Internal error (details are logged server-side, not returned). | ```bash Example Request theme={null} curl 'https://listenlabs.ai/api/public/v1/studies/9b2f1c3e-0000-0000-0000-000000000000/questions' \ -H 'x-api-key: ' ``` ```json Response theme={null} { "questions": [ { "id": "a1b2c3d4-0000-0000-0000-000000000001", "text": "How often do you drink coffee?", "isScreener": true, "questionNumber": 1, "type": "multiple_choice", "isMultiSelect": false, "options": ["Every day", "A few times a week", "Rarely or never"], "concepts": [] }, { "id": "a1b2c3d4-0000-0000-0000-000000000002", "text": "Tell me about the last time you tried a new coffee brand.", "isScreener": false, "questionNumber": 2, "type": "open_ended", "concepts": [] }, { "id": "a1b2c3d4-0000-0000-0000-000000000003", "text": "What's your first impression of this design?", "isScreener": false, "questionNumber": 3, "type": "open_ended", "concepts": [ { "id": "c1d2e3f4-0000-0000-0000-000000000001", "title": "Minimal design", "description": "Clean white packaging with a single accent color.", "media": [ { "type": "image", "name": "minimal.png", "url": "https://example.com/minimal.png" } ], "embedUrl": null } ] }, { "id": "a1b2c3d4-0000-0000-0000-000000000004", "text": "How satisfied are you with each of the following?", "isScreener": false, "questionNumber": 4, "type": "matrix", "isMultiSelect": false, "rows": ["Taste", "Price", "Availability"], "columns": ["Not satisfied", "Somewhat satisfied", "Very satisfied"], "concepts": [] }, { "id": "a1b2c3d4-0000-0000-0000-000000000005", "text": "Rank these brands from most to least trusted.", "isScreener": false, "questionNumber": 5, "type": "ranking", "options": ["Blue Bottle", "Stumptown", "Lavazza"], "concepts": [] }, { "id": "a1b2c3d4-0000-0000-0000-000000000006", "text": "Which of these matters most and least when choosing a coffee?", "isScreener": false, "questionNumber": 6, "type": "max_diff", "options": ["Price", "Origin", "Roast level", "Brand"], "metric": "importance", "itemsPerScreen": 4, "concepts": [] } ] } ``` # Launch Study Source: https://docs.listenlabs.ai/api-v2/launch-study api-v2/openapi.yaml POST /api/public/v1/studies/{studyId}/launch Publish a draft study and get its self-recruit link # Get Responses Source: https://docs.listenlabs.ai/api-v2/list-responses api-v2/openapi.yaml GET /api/public/v1/responses/{linkId} List a study's responses with answers and summaries, paginated # List Studies Source: https://docs.listenlabs.ai/api-v2/list-studies api-v2/openapi.yaml GET /api/public/v1/studies List studies with titles, response counts, creators, and folder paths ## Study metadata Each study includes `creator` and `folderPath` alongside its identifiers, title, creation time, and completed response count. Both fields are always present but may be `null`. * `creator` contains the creator's `name` and `email`. Either nested value may also be `null`. * `folderPath` contains the study's folder path as an array of strings. # List Wallets Source: https://docs.listenlabs.ai/api-v2/list-wallets api-v2/openapi.yaml GET /api/public/v1/wallets List the wallets granted to your organization, with credit balances # Listen Labs API Source: https://docs.listenlabs.ai/api-v2/overview Create, launch, and retrieve data from Listen Labs studies programmatically The Listen Labs API lets you run studies entirely from code: define a study guide as JSON, create a draft, launch it to get a self-recruit link you can distribute to participants, and pull responses back out once interviews come in. ## Base URL ``` https://listenlabs.ai ``` ## Authentication All endpoints authenticate with an API key passed in the `x-api-key` header. The key is scoped to a single organization — studies are created in, and wallets are listed for, that organization. ```bash theme={null} curl 'https://listenlabs.ai/api/public/v1/wallets' \ -H 'x-api-key: ' ``` Admins and Supervisors can create API keys from the **Developer** section of their account page on Listen. See [Get API Access](/get-api-access) for details. ## Workflow `POST /api/public/v1/studies/create` with a title and a [study guide](/api-v2/study-guide). The guide is validated up front; if it passes, you get back a draft study's `id` and `linkId`. Nothing is visible to participants yet — you can still review or edit the draft in the dashboard. `GET /api/public/v1/wallets` lists the wallets granted to your organization with their recruitment and project credit balances. `walletId` can only be omitted at launch when your organization has exactly one wallet (it's auto-selected). If you have access to more than one, omitting it returns a `400` (`wallet_required`) — you must pass one from this list. `POST /api/public/v1/studies/{studyId}/launch` publishes the draft and opens its self-recruit link. The response includes `selfRecruitLink` — share it with participants (or plug it into your own recruitment flow). Project responses bill to the launch wallet. Once interviews come in, pull them with [Get Responses](/api-v2/list-responses) (`GET /api/public/v1/responses/{linkId}`) and drill into a single transcript with [Get Single Response](/api-v2/get-response). The `linkId` comes back from create/launch, or from [List Studies](/api-v2/list-studies). The [Data Map](/data-map) shows how these entities join together. A complete create → launch example you can copy and run. ## Endpoints | Endpoint | Description | | ------------------------------------------------------------------------------- | ------------------------------------------------------------- | | [`POST /api/public/v1/studies/create`](/api-v2/create-study) | Validate a study guide and create a draft study | | [`POST /api/public/v1/studies/{studyId}/launch`](/api-v2/launch-study) | Publish a draft and return the self-recruit link | | [`GET /api/public/v1/wallets`](/api-v2/list-wallets) | List the organization's wallets with balances | | [`GET /api/public/v1/studies`](/api-v2/list-studies) | List studies with response counts, creators, and folder paths | | [`GET /api/public/v1/studies/{studyId}/questions`](/api-v2/get-study-questions) | Get a study's questions and concepts | | [`GET /api/public/v1/responses/{linkId}`](/api-v2/list-responses) | List all responses for a study | | [`GET /api/public/v1/responses/{linkId}/{responseId}`](/api-v2/get-response) | Retrieve a single response | ## Errors Every error response from the `/api/public/v1/*` endpoints shares one JSON envelope: * `error` — a human-readable message. Useful for logs, but it may change; **don't** branch on it. * `code` — a stable, machine-readable identifier. **Branch on this.** * `issues` — present only on `400` schema failures (`code: invalid_request_body`): an array of per-field violations, each with a `path` and `message`. ```json 400 Schema violation theme={null} { "error": "Invalid request body", "code": "invalid_request_body", "issues": [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": ["title"], "message": "Required" } ] } ``` ```json 400 Invalid study guide theme={null} { "error": "Invalid study guide: conditional references unknown item externalId 'q2'", "code": "invalid_study_guide" } ``` ### Error codes | Status | Code | Meaning | | ------ | -------------------------- | ------------------------------------------------------------------------------------- | | `400` | `invalid_json` | Request body isn't valid JSON. | | `400` | `invalid_request_body` | Body failed schema validation. See `issues` for per-field details. | | `400` | `invalid_study_guide` | The study guide broke a cross-field rule (create only). | | `400` | `wallet_required` | The organization has multiple wallets and `walletId` was omitted (launch only). | | `400` | `insufficient_credits` | The wallet can't fund the launch — insufficient balance or grant limit (launch only). | | `400` | `bad_request` | A query or path parameter is invalid (response endpoints only). | | `401` | `missing_api_key` | No `x-api-key` header. | | `401` | `invalid_api_key` | The API key is invalid. | | `403` | `forbidden` | The key's user isn't a member of the key's organization. | | `403` | `launch_permission_denied` | The key's user lacks permission to launch this study. | | `403` | `wallet_access_denied` | No access to the specified wallet. | | `404` | `study_not_found` | Study not found in the key's organization. | | `404` | `response_not_found` | Response not found in the study (get response only). | | `409` | `concurrent_modification` | The draft was modified concurrently; retry (create only). | | `409` | `study_busy` | An update is in flight; retry after a moment (launch only). | | `409` | `conflict` | The publish raced a concurrent edit; refresh and retry (launch only). | | `500` | `internal_error` | Internal error. Details are logged server-side, not returned. | Beyond field-level validation, the server enforces cross-field rules on the study guide (screening block placement, `minSelect`/`maxSelect` coupling, unique `externalId`s, reference resolution, `exclusiveOption` placement) and returns violations as `400` responses (`code: invalid_study_guide`). The full list is in the [Study Guide Reference](/api-v2/study-guide#validation-rules). # Quickstart: Create & Launch a Study Source: https://docs.listenlabs.ai/api-v2/quickstart Go from JSON to a live self-recruit link in three requests This walkthrough creates a study with a screener, an interview section, and conditional logic — then launches it and gets a link you can send to participants. You'll need an API key (see [Get API Access](/get-api-access)). The key is scoped to one organization; the study is created there. ## 1. Create a draft study `POST /api/public/v1/studies/create` takes the study definition and validates it. Note the `externalId` on the multiple choice question — the later open-ended question references it in a conditional. ```bash Request theme={null} curl -X POST 'https://listenlabs.ai/api/public/v1/studies/create' \ -H 'x-api-key: ' \ -H 'Content-Type: application/json' \ -d '{ "title": "Coffee habits — API demo", "externalTitle": "A short interview about your coffee routine", "background": "We are a specialty coffee brand exploring how people choose what to buy.", "studyGoal": "Understand what drives brand switching among regular coffee drinkers.", "config": { "interviewMode": "audio_text", "questionLanguage": "en" }, "welcomeMessage": { "title": "Thanks for joining!", "message": "This interview takes about 10 minutes. There are no wrong answers." }, "closingMessage": "That is all — thank you for your time!", "studyGuide": [ { "type": "screening", "title": "Screener", "items": [ { "type": "multiple_choice", "text": "How often do you drink coffee?", "options": [ { "text": "Every day", "status": "approve" }, { "text": "A few times a week", "status": "approve" }, { "text": "Rarely or never", "status": "reject" } ] } ] }, { "type": "flat", "title": "Interview", "items": [ { "externalId": "purchase-channels", "type": "multiple_choice", "text": "Where do you usually buy coffee?", "multiSelect": true, "options": [ { "externalId": "opt-cafe", "text": "Cafés" }, { "externalId": "opt-grocery", "text": "Grocery stores" }, { "externalId": "opt-online", "text": "Online" }, { "text": "Somewhere else", "exclusiveOption": true } ] }, { "type": "open_ended", "text": "What do you like about buying coffee online?", "followUp": "medium", "conditional": { "operator": "and", "criteria": [ { "type": "selectedTemplateOptions", "questionId": "purchase-channels", "matchingCriteria": "mustSelect", "choices": ["opt-online"] } ] } }, { "type": "open_ended", "text": "Tell me about the last time you tried a new coffee brand.", "followUp": "heavy" } ] } ] }' ``` ```json Response (201) theme={null} { "id": "9b2f1c3e-0000-0000-0000-000000000000", "linkId": "coffee-habits-api-demo", "status": "draft" } ``` The study is now a **draft** — participants can't see it yet, and you can review or tweak it in the dashboard before launching. If validation fails, you get a `400` whose `code` tells you what went wrong (`invalid_request_body` includes an `issues` array pointing at the offending fields; `invalid_study_guide` flags a broken cross-field rule). Branch on `code`, not on the human-readable `error` text. ## 2. Find your wallet Launching bills project responses to a wallet. `walletId` can only be omitted when your organization has exactly one wallet — launch then auto-selects it. With multiple wallets you must pass one explicitly, so list them and pick: ```bash Request theme={null} curl 'https://listenlabs.ai/api/public/v1/wallets' \ -H 'x-api-key: ' ``` ```json Response (200) theme={null} { "wallets": [ { "walletId": "5e8a7d40-0000-0000-0000-000000000000", "name": "Research team", "recruitmentCreditBalance": { "balance": 480, "usage": 20 }, "projectCreditBalance": { "balance": 950, "usage": 50 } } ] } ``` `usage` includes active holds. ## 3. Launch The path takes the study's `id` from step 1 — the `linkId` is not accepted here. ```bash Request theme={null} curl -X POST 'https://listenlabs.ai/api/public/v1/studies/9b2f1c3e-0000-0000-0000-000000000000/launch' \ -H 'x-api-key: ' \ -H 'Content-Type: application/json' \ -d '{ "walletId": "5e8a7d40-0000-0000-0000-000000000000" }' ``` ```json Response (200) theme={null} { "id": "9b2f1c3e-0000-0000-0000-000000000000", "linkId": "coffee-habits-api-demo", "selfRecruitLink": "https://listenlabs.ai/s/coffee-habits-api-demo", "status": "live", "wallet": { "walletId": "5e8a7d40-0000-0000-0000-000000000000", "name": "Research team", "recruitmentCreditBalance": { "balance": 480, "usage": 20 }, "projectCreditBalance": { "balance": 950, "usage": 50 } } } ``` The study is live. Share `selfRecruitLink` with participants — you can also append URL parameters (e.g. `?segment=pro`) and route on them with [`searchParam` conditionals](/api-v2/study-guide#conditional-logic) or read them back later from each response's `urlParams`. If you omit the body and your organization has multiple wallets, launch returns `400` with `code: wallet_required` asking for an explicit `walletId`. A `409` (`code: study_busy` or `conflict`) means the study is mid-publish — retry after a moment. ## 4. Collect the results Once responses come in, pull them with the data endpoints using the `linkId` (the study's `id` is accepted too): ```bash theme={null} curl 'https://listenlabs.ai/api/public/v1/responses/coffee-habits-api-demo' \ -H 'x-api-key: ' ``` All block and question types, conditionals, carry-forward, and validation rules. Retrieve transcripts, answers, and summaries for a launched study. # Study Guide Reference Source: https://docs.listenlabs.ai/api-v2/study-guide How to structure the studyGuide payload: blocks, question types, screening, concepts, conditional logic, and carry-forward The `studyGuide` field of [Create Study](/api-v2/create-study) defines everything participants see: the questions, their order, screening, concept testing, and routing logic. It's an array of **blocks**, and each block contains one or more **items** (questions). ```json Minimal study theme={null} { "title": "Coffee habits interview", "studyGuide": [ { "type": "flat", "title": "Main questions", "items": [ { "type": "open_ended", "text": "Walk me through your morning coffee routine." } ] } ] } ``` ## Top-level request fields Internal study title (shown in the dashboard). Participant-facing title. Falls back to `title` when omitted. Background context for the AI interviewer — what the study is about and who you're talking to. What you want to learn. Helps the AI interviewer probe in the right direction. Interview mode, languages, and platform targeting. See [Config](#config). `{ "title": "...", "message": "..." }` shown to participants before the interview starts. Message shown when the interview ends. The blocks described below. At least one. ## Config One of `text`, `audio`, `audio_text`, `audio_screen`, `video`, `video_screen`. Language code the questions are written in (e.g. `en`, `de`, `fr`, `es`, `zh-TW`, `en-medical`). See the [complete language list](/setup-to-launch/languages-complete-list). Translation target codes participants can switch to. `null` or omitted = English only. Restrict which devices can take the study: any of `ios`, `android`, `desktop`. ```json Example config theme={null} { "config": { "interviewMode": "audio_text", "questionLanguage": "en", "availableLanguages": ["en", "de", "fr"], "targetPlatforms": ["desktop"] } } ``` ## Blocks Every block has a `type`, a `title`, and an `items` array (at least one item). | Type | Purpose | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `flat` | A plain sequence of questions. | | `screening` | Qualifies participants before the interview. At most one, and it must be the **first** block. | | `concept` | Shows each participant one or more concepts (stimuli) and asks the block's questions about them. Requires `conceptSamplingConfig`. | ### Screening blocks Screening blocks may contain **only `multiple_choice` items**, and every option must carry a `status`: * `approve` — selecting it qualifies the participant * `reject` — selecting it screens the participant out * `neutral` — doesn't affect qualification ```json Screening block theme={null} { "type": "screening", "title": "Screener", "items": [ { "externalId": "coffee-frequency", "type": "multiple_choice", "text": "How often do you drink coffee?", "options": [ { "externalId": "opt-daily", "text": "Every day", "status": "approve" }, { "text": "A few times a week", "status": "approve" }, { "text": "Rarely or never", "status": "reject" } ] } ] } ``` Outside screening blocks, `status` must be `null` or omitted. ### Concept blocks Concept blocks show stimuli — product ideas, ads, prototypes — and ask the block's items about each one. They require a `conceptSamplingConfig` with at least one concept; other block types must omit it. Each concept has a `nickname` (internal label) and `content`: a participant-facing `title`, optional `description`, optional `media` (images/videos), and an optional `embed` (e.g. a Figma prototype URL). How many concepts each participant sees, randomly sampled. `null` = every participant sees all concepts. Each concept can also carry a [`conditional`](#conditional-logic), so it's only shown to participants matching the criteria — e.g. gate each concept on a screener answer. `selectedTemplateOptions` criteria on a concept must reference an item in a block **before** the concept block. ```json Concept block theme={null} { "type": "concept", "title": "Packaging concepts", "conceptSamplingConfig": { "subsampleCount": 2, "concepts": [ { "nickname": "minimal", "content": { "title": "Minimal design", "description": "Clean white packaging with a single accent color.", "media": [ { "name": "minimal.png", "url": "https://example.com/minimal.png", "type": "image" } ] } }, { "nickname": "bold", "content": { "title": "Bold design", "embed": { "url": "https://www.figma.com/proto/..." } }, "conditional": { "operator": "and", "criteria": [ { "type": "selectedTemplateOptions", "questionId": "coffee-frequency", "matchingCriteria": "mustSelect", "choices": ["opt-daily"] } ] } } ] }, "items": [ { "type": "open_ended", "text": "What's your first impression of this design?" } ] } ``` ## Question types All items share a few common fields: The question text. One of `open_ended`, `multiple_choice`, `ranking`, `matrix`, `max_diff`, `statement`. Stable identifier for the item, unique within the payload. Auto-generated when omitted — set it only when a [conditional](#conditional-logic) or [`carryForwardFrom`](#carry-forward) needs to reference this item. Show this question only when the criteria match. See [Conditional logic](#conditional-logic). Images or videos shown with the question: `{ "name", "url", "type": "image" | "video", "widthPercentage"?, "forceWatching"? }`. `forceWatching` requires the participant to finish the video before answering. An embedded web page shown with the question: `{ "url", "proxyUrl"? }`. ### Open-ended The AI interviewer asks the question conversationally and can probe with follow-ups. Follow-up depth: `none`, `light`, `medium`, or `heavy`. Extra instructions for the AI interviewer on how to probe this question. `text`, `voice`, `screenRecording`, or `none`. Lets the AI observe the participant's screen while they answer. Only applies when `preferredInput` is `screenRecording`. ```json Open-ended theme={null} { "type": "open_ended", "text": "Tell me about the last time you switched coffee brands.", "followUp": "medium", "addInstructions": "Probe for what triggered the switch and what almost stopped them.", "preferredInput": "voice" } ``` ### Multiple choice Required unless `carryForwardFrom` is set. Each option: `{ "text", "externalId"?, "status"?, "exclusiveOption"? }`. Allow selecting more than one option. Bounds on how many options must be selected. Both must be set together (or both omitted), only valid when `multiSelect` is `true`, and `minSelect` ≤ `maxSelect`. Adds an "Other" option with free-text input. Shuffle option order per participant. Keep the last option in place when randomizing (e.g. "None of the above"). `externalId` of an earlier multi-select multiple choice item; this question shows only the options the participant selected there. See [Carry-forward](#carry-forward). An option with `exclusiveOption: true` clears and locks the other selections when picked (e.g. "None of the above"). It's only valid on the **final** option of a multi-select question. ```json Multiple choice theme={null} { "externalId": "brands-used", "type": "multiple_choice", "text": "Which coffee brands have you bought in the last 3 months?", "multiSelect": true, "minSelect": 1, "maxSelect": 4, "randomizeOptionOrder": true, "pinnedFinalOption": true, "options": [ { "externalId": "opt-blue-bottle", "text": "Blue Bottle" }, { "externalId": "opt-stumptown", "text": "Stumptown" }, { "externalId": "opt-lavazza", "text": "Lavazza" }, { "text": "None of the above", "exclusiveOption": true } ] } ``` ### Ranking Participants order the options. Supports `randomizeOptionOrder` and `carryForwardFrom` (rank only the options selected in an earlier multi-select question). ```json Ranking theme={null} { "type": "ranking", "text": "Rank these brands from most to least trusted.", "carryForwardFrom": "brands-used" } ``` ### Matrix A grid: each **row** is rated single-select across the **options** (columns). ```json Matrix theme={null} { "externalId": "satisfaction-matrix", "type": "matrix", "text": "How satisfied are you with each of the following?", "options": [ { "text": "Not satisfied" }, { "text": "Somewhat satisfied" }, { "text": "Very satisfied" } ], "rows": [ { "text": "Taste" }, { "text": "Price" }, { "text": "Availability" } ] } ``` ### MaxDiff Best/worst scaling across at least two options. See [MaxDiff analysis](/insights-and-reports/max-diff-questions) for how results are reported. The items being compared (minimum 2). What participants judge the items on (e.g. "importance", "appeal"). How many items are shown per comparison screen. ```json MaxDiff theme={null} { "type": "max_diff", "text": "Which of these matters most and least when choosing a coffee?", "metric": "importance", "itemsPerScreen": 4, "options": [ { "text": "Price" }, { "text": "Origin" }, { "text": "Roast level" }, { "text": "Brand" }, { "text": "Packaging" } ] } ``` ### Statement Not a question — shows text (and optional media/embed) with a continue button. Use it for instructions or section intros. Custom label for the continue button. ```json Statement theme={null} { "type": "statement", "text": "Next, we'll show you a few packaging designs. There are no right or wrong answers.", "continueButtonText": "Show me" } ``` ## Conditional logic Any item — and any [concept](#concept-blocks) in a concept block — can carry a `conditional`: it's only shown when the criteria match. Criteria are combined with an `operator` (`and` / `or`). ```json Show only if they selected Blue Bottle theme={null} { "type": "open_ended", "text": "What keeps you coming back to Blue Bottle?", "conditional": { "operator": "and", "criteria": [ { "type": "selectedTemplateOptions", "questionId": "brands-used", "matchingCriteria": "mustSelect", "choices": ["opt-blue-bottle"] } ] } } ``` Two criterion types: **`selectedTemplateOptions`** — based on an answer to an earlier `multiple_choice` or `matrix` item. `externalId` of an earlier `multiple_choice` or `matrix` item. For a conditional on a concept, the item must live in a block **before** the concept block. `mustSelect` or `mustNotSelect`. Option `externalId`s (falls back to option text). For a matrix source, these are the columns. Required only for a matrix source: the exact row text the criterion applies to. **`searchParam`** — based on a URL parameter passed into the study link. The query parameter name, e.g. `segment` in `https://listenlabs.ai/s/abc123?segment=pro`. The value it must equal. See also [Conditional logic](/setup-to-launch/conditional-logic) for how this behaves in the interview. ## Carry-forward `multiple_choice` and `ranking` items can set `carryForwardFrom` to the `externalId` of an **earlier multi-select multiple choice** item. The question then only shows (or ranks) the options the participant actually selected there — so you can ask "which have you used?" followed by "which of *those* do you prefer?". When `carryForwardFrom` is set, omit `options`; they're inherited from the source question. ## External IDs `externalId`s are stable handles you assign to blocks, items, options, and concepts. They're optional everywhere — the server auto-generates them when omitted — but you must set one on any item that a `conditional` (`questionId`) or `carryForwardFrom` references. Block, item, and concept `externalId`s must be unique across the entire payload. ## Validation rules Beyond field-level validation, the server enforces these cross-field rules, returning `400` with `code: invalid_study_guide` and a descriptive `error` message when violated: * At most **one screening block**, and it must be the **first** block. * Screening blocks may contain only `multiple_choice` items, and every option in them must carry a `status`; outside screening blocks, `status` must be null/omitted. * Concept blocks require `conceptSamplingConfig` with at least one concept; other block types must omit it. * `externalId`s must be unique across blocks, items, and concepts. * `conditional.criteria[].questionId` and `carryForwardFrom` must reference the `externalId` of an **earlier** item (of the right type). * A concept's `conditional` must reference an item in a block **before** the concept block. * `minSelect`/`maxSelect` must both be set or both omitted, only when `multiSelect` is true, with `minSelect` ≤ `maxSelect`. * `exclusiveOption` is only valid on the final option of a multi-select `multiple_choice` question. # Data Map Source: https://docs.listenlabs.ai/data-map How the public API entities relate to each other and which fields to use as join keys. ## Join Keys | From | To | Join field | Notes | | -------- | ------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Study | Response | `linkId` | Path param in `GET /api/public/v1/responses/{linkId}`. Accepts the study's `linkId` or its UUID `id`. Single response returns `linkId`. | | Study | Question | `id` | Path param in `GET /api/public/v1/studies/{studyId}/questions`. Accepts the study's UUID `id` or its `linkId`. | | Question | Answer | `discussionGuideQuestionId` | `Question.id` = `Answer.discussionGuideQuestionId` | | Question | TranscriptRow | `discussionGuideQuestionId` | `Question.id` = `TranscriptRow.discussionGuideQuestionId` | | Answer | TranscriptRow | `answerId` | One answer can have multiple transcript rows (follow-ups where `isFollowUp: true`). | | Question | Concept | `concepts` | Array on the Question object. `conceptId` on Answer/TranscriptRow links to `Concept.id`. Empty when the question has no concepts. | ## Entity Fields ### Study Returned by [List Studies](/api-v2/list-studies). | Field | Type | Description | | --------------- | ----------------- | ------------------------------------------------------------------------------------------------- | | `id` | uuid | Permanent study identifier — the path param for launch, also accepted for questions and responses | | `linkId` | string | Editable URL slug — accepted by the questions and response endpoints | | `title` | string | Study title | | `responseCount` | number | Completed response count | | `createdAt` | string | UTC timestamp | | `creator` | object \| null | Study creator. Contains `name` and `email`, each of which may be null. | | `folderPath` | string\[] \| null | Study folder path as an array of strings. | ### Response Returned by [Get Responses](/api-v2/list-responses). Each response belongs to a study via `linkId`. | Field | Type | Description | | ------------------------- | ---------------- | -------------------------------------------------------------------------------------------- | | `id` | uuid | Response identifier | | `readableId` | number | Order within the study | | `progress` | string | `"complete"`, `"screened_out"`, or `"in_progress"` | | `responseDurationSeconds` | number | Total duration | | `qualityScore` | number \| string | Quality rating | | `answers` | object | Keyed by question text, plus a `"Summary"` key (e.g. `"Summary"`, `"Q1: What is your age?"`) | | `answersArray` | Answer\[] | Structured answers, joinable to questions and transcript | | `urlParams` | object | URL params passed into the study | | `tags` | string\[] | Keywords | | `tagline` | string \| null | One-line synthesis | | `bulletSummary` | string\[] | Bullet-point summary | | `shortTranscript` | string \| null | Whole conversation with assistant messages shortened to a few words | | `shortAssistantMessages` | string\[] | Condensed assistant turns (optional) | | `otherRemarks` | string | Additional remarks (optional) | | `personas` | string | Persona classification (optional) | | `createdAt` | string | UTC timestamp | | `updatedAt` | string | UTC timestamp | ### Answer Nested inside `Response.answersArray`. | Field | Type | Description | | --------------------------- | -------------- | ------------------------------------------------------------------------------------------ | | `answerId` | string | **Join key** — matches `TranscriptRow.answerId` | | `discussionGuideQuestionId` | string \| null | **Join key** — matches `Question.id`. Null when the answer isn't tied to a guide question. | | `conceptId` | string \| null | Links to `Concept.id` when the question involves concept testing | | `question` | string | Question text | | `answer` | string | Answer text | ### TranscriptRow Returned by [Get Single Response](/api-v2/get-response) inside the `transcript` array. | Field | Type | Description | | --------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------- | | `moderator` | string | Assistant/moderator text | | `user` | string | Participant text | | `discussionGuideQuestionId` | string \| null | **Join key** — matches `Question.id`. Null for non-question rows (e.g. intro). | | `answerId` | string \| null | **Join key** — matches `Answer.answerId`. Null for non-question rows (e.g. intro). | | `conceptId` | string \| null | Links to `Concept.id` | | `responseIndex` | number | Zero-based row index | | `isFollowUp` | boolean | `true` when this row is a follow-up to the same question as the previous row | | `audio` | string \| null | Signed URL (\~1 hour validity) | | `video` | object \| null | Camera recording — contains `streamUrl` (HLS) and `mp4Url` | | `screenVideo` | object \| null | Screen recording (when the study captured the participant's screen) — contains `streamUrl` (HLS) and `mp4Url` | ### Question Returned by [Get Study Questions](/api-v2/get-study-questions). | Field | Type | Description | | -------------------------- | -------------- | -------------------------------------------------------------------------------------------- | | `id` | string | **Join key** — referenced as `discussionGuideQuestionId` on answers and transcript rows | | `text` | string | Question text shown to participants | | `type` | string | `open_ended`, `file_upload`, `multiple_choice`, `ranking`, `statement`, `matrix`, `max_diff` | | `questionNumber` | number | Display order | | `isScreener` | boolean | Whether this is a screener question | | `options` | string\[] | Choices (for `multiple_choice`, `ranking`, and `max_diff` types) | | `isMultiSelect` | boolean | Whether multiple selections are allowed (`multiple_choice` and `matrix` only) | | `rows`, `columns` | string\[] | Matrix rows and columns (`matrix` only) | | `metric`, `itemsPerScreen` | string, number | MaxDiff metric and items shown per screen (`max_diff` only) | | `concepts` | Concept\[] | Concept definitions attached to this question | ### Concept Nested inside `Question.concepts`. | Field | Type | Description | | ------------- | -------------- | ----------------------------------------------------- | | `id` | string | Referenced by `conceptId` on Answer and TranscriptRow | | `title` | string | Concept title | | `description` | string | Concept description | | `media` | Media\[] | Attached images/videos (`type`, `name`, `url`) | | `embedUrl` | string \| null | External embed (e.g. Figma link) | ## Traversal Examples ### Get all answers for a study, grouped by question 1. `GET /api/public/v1/studies` — find the study's `id` and `linkId` 2. `GET /api/public/v1/studies/{studyId}/questions` — get all questions (using the `id`) 3. `GET /api/public/v1/responses/{linkId}` — get all responses 4. Join each `answersArray[]` item to its question using `discussionGuideQuestionId` = `Question.id` ### Match transcript audio/video to specific questions 1. `GET /api/public/v1/responses/{linkId}/{responseId}` — get the full transcript 2. `GET /api/public/v1/studies/{studyId}/questions` — get question definitions 3. Each transcript row's `discussionGuideQuestionId` tells you which question it belongs to 4. Use `audio`, `video`, and `screenVideo` fields on the transcript row to access the media for that exchange ### Group transcript rows by answer (including follow-ups) 1. `GET /api/public/v1/responses/{linkId}/{responseId}` — get the full transcript 2. Group transcript rows by `answerId` — rows sharing the same `answerId` belong to the same answer, with follow-ups marked by `isFollowUp: true` 3. Cross-reference with `answersArray` from the list endpoint using the same `answerId` to get the extracted answer text # Authentication Source: https://docs.listenlabs.ai/get-api-access ## API key 1. Go to your account page on Listen and open the **Developer** section. API keys can be created by Admins and Supervisors. 2. Create an API key. 3. Use it in your request in the `x-api-key` header. ``` 'x-api-key': 'abc123....' ``` Each API key is scoped to a single organization. It works with all [versioned API endpoints](/api-v2/overview) (`/api/public/v1/...`) — studies, questions, wallets, and responses — as well as the legacy unversioned data endpoints (`/responses`, `/list_surveys`, questions). # Welcome to Listen Labs Source: https://docs.listenlabs.ai/get-started/index Listen Labs is an AI-powered research platform. Start with a business question. Listen handles the rest. > From study design and recruitment to complex analysis workflows. Testing new product concepts or testing new markets. Listen is your trusted research partner that will deliver insights in hours instead of weeks. This is your comprehensive guide to the Listen platform, covering everything you need to get started and launch your first study. If you'd rather see it in action first, watch the video below where Camille, our Head of Insights Strategy, walks you through the platform in depth.