💻

Developer Documentation

3 articles James By James

Build with the UserLoop API, SDK, and Model Context Protocol server.

UserLoop API

Getting Started - Base URL: https://api.userloop.io - Generate API keys inside the UserLoop dashboard. Keys are tied to a company, scoped by feature, and shown only once at creation time. - Keep keys private. Rotate immediately if you suspect compromise. Authenticate Every Request Send the full key string exactly as issued using the X-API-Key header: curl https://api.userloop.io/health \ -H "X-API-Key: " The API validates scopes, survey allowlists, and optional origin restrictions before serving any protected data. Date & Pagination Helpers - start_date and end_date must use YYYY-MM-DD format. When omitted, endpoints default to the broadest safe range (from 1970-01-01 through the current day) so analytics always receive explicit boundaries. - limit and offset control pagination. limit defaults to 50 (maximum 200). offset defaults to 0. Endpoint Reference 1. Health Check — GET /health Verifies the service is reachable. No authentication required. { "status": "ok" } 2. Survey Catalog — GET /surveys Lists surveys accessible to the calling API key. Query parameters: | Name | Type | Description | | ---- | ---- | ---- | | company_id | string (optional) | Validate the company owning the key. If provided it must match the key’s company; otherwise it defaults automatically. | Example request: curl "https://api.userloop.io/surveys" \ -H "X-API-Key: " Example response (trimmed for brevity): { "company_id": "1621...", "count": 2, "surveys": [ { "id": "1624...", "title": "Post Purchase Email Survey", "format": "Email", "status": "active", "question_count": 6, "created_at": "2021-05-19T10:52:50.657Z", "updated_at": "2025-10-10T12:10:34.630Z", "toggles": { "progress_bar": true, "discount_enabled": true }, "schedule": { "post_purchase": "in 2 days" }, "discount": { "header": "Your 10% Coupon Awaits", "shopify_price_rules": [ { "api_c2_id": "1032299675825" } ] } } ] } Response fields | Field | Type | Description | | ---- | ---- | ---- | | company_id | string | Company ID | | count | integer | Number of surveys returned. | | surveys | array | Collection of survey summaries ordered by last update. | | surveys[].id | string | Unique survey identifier. | | surveys[].title | string | Human readable survey name. | | surveys[].format | string | Channel (Email, Checkout, Link, etc.). | | surveys[].status | string | active or archived. | | surveys[].question_count | integer | Number of questions associated with the survey. | | surveys[].question_ids | array | List of question IDs. | | surveys[].created_at / updated_at | ISO 8601 string | Creation and last modification timestamps. | | surveys[].toggles, schedule, triggers, discount, colors, recipients, flags, incentives, sharing, integrations | object | Grouped metadata copied from the survey configuration. Keys are normalized to snake_case. | 3. Survey Metadata — GET /surveys/{survey_id} Retrieves the full configuration (questions, answer choices, metadata) for a single survey. curl "https://api.userloop.io/surveys/1710884429805x361876466030084100" \ -H "X-API-Key: " { "survey_id": "1710884429805x361876466030084100", "survey": "Post Purchase Email Survey", "company_id": "1621...", "questions": [ { "question": "How satisfied were you with your recent order?", "question_id": "1710884436289x589566297607766000", "type": "CSAT", "answers": [ { "answer": "1", "answer_id": "1658..." }, { "answer": "2", "answer_id": "1659..." } ] } ] } Response fields | Field | Type | Description | | ---- | ---- | ---- | | survey_id | string | Survey identifier. | | survey | string | Survey title. | | company_id | string | Owning company. | | questions | array | Ordered list of questions in the survey. | | questions[].question_id | string | Question identifier used in analytics queries. | | questions[].question | string | Question text. | | questions[].type | string | Question type (CSAT, NPS, etc.). | | questions[].answers | array | Answer options (when applicable). | | answers[].answer_id | string | Answer identifier used in analytics filters. | | answers[].answer | string | Display text for the answer choice. | 4. Aggregated Analytics — GET /responses?view=counts Calculates response counts, percentages, and revenue metrics per answer option. Required query parameters: | Name | Type | Description | | ---- | ---- | ---- | | survey_id | string | Survey to analyze. Must be enabled for the calling key. | | question_id | string | Question to aggregate. | Optional query parameters: | Name | Type | Description | | ---- | ---- | ---- | ---- | ---- | ---- | ---- | | answer_ids | comma-separated strings | Restrict analytics to specific answer IDs. Defaults to all answers in the question. | | start_date, end_date | YYYY-MM-DD | Restrict analytics to a date window. Defaults to full history. | Example request: curl "https://api.userloop.io/responses?view=counts&survey_id=1710884429805x361876466030084100&question_id=1710884436289x589566297607766000&start_date=2025-09-01&end_date=2025-09-30" \ -H "X-API-Key: " Example response (abridged): { "data": [ { "answer_id": "1658...", "answer_text": "1", "count": 75, "percentage": 50, "revenue": 15000, "aov": 200, "currency": "USD" } ], "meta": { "survey_id": "1710884429805x361876466030084100", "survey": "Post Purchase Email Survey", "question": { "id": "1710884436289x589566297607766000", "text": "How satisfied were you with your recent order?", "type": "CSAT" }, "totals": { "responses": 150, "unique_customers": 120, "sum_responses": 150 }, "filters": { "answer_ids": ["1658...", "1659..."], "start_date": "2025-09-01", "end_date": "2025-09-30" } } } Response fields | Field | Type | Description | | ---- | ---- | ---- | | data | array | Aggregated metrics per answer option (sorted by count desc). | | data[].answer_id | string | Answer identifier. | | data[].answer_text | string | Answer label (resolved from survey config when available). | | data[].count | integer | Number of responses recorded for the answer. | | data[].percentage | number | Share of total responses for the answer (0–100). | | data[].revenue | integer | Sum of order_total values associated with the answer (rounded). | | data[].aov | integer | Average order value for the answer (rounded). | | data[].currency | string | Currency code used for revenue metrics. | | meta | object | Contextual metadata for the aggregation. | | meta.survey_id | string | Survey identifier. | | meta.survey | string | Survey title (if available). | | meta.question | object | Question metadata (id, text, type). | | meta.totals.responses | integer | Count of responses returned by analytics (count_survey_responses). | | meta.totals.unique_customers | integer | Unique respondent count. | | meta.totals.sum_responses | integer | Sum of data[].count; falls back to total responses when rows are empty. | | meta.filters | object | Effective filters applied to the analytics call. | | meta.filters.answer_ids | array | Answer IDs used for aggregation. | | meta.filters.start_date / end_date | string | ISO dates bounding the analytics query. | 5. Open-Ended Feedback — GET /responses?view=open Fetches paginated free-text responses with associated metadata. Required query parameters: survey_id, question_id Optional query parameters: start_date, end_date, limit, offset Example request: curl "https://api.userloop.io/responses?view=open&survey_id=1710884429805x361876466030084100&question_id=1658178244899x246576140312903680&limit=50" \ -H "X-API-Key: " Example response (first record shown): { "data": [ { "id": "abc123", "unique_id": "abc123", "recipient": "[email protected]", "survey": "1710884429805x361876466030084100", "creation_date": "2025-09-10T12:00:00Z", "open_ended_response": "Great product!", "line_items": ["Product A"], "line_items_count": 1, "order_total": 199.99, "currency": "USD" } ], "pagination": { "total_count": 150, "page_size": 50, "current_page": 1, "total_pages": 3, "has_next_page": true, "has_previous_page": false }, "filters": { "survey_id": "1710884429805x361876466030084100", "question_id": "1658178244899x246576140312903680", "start_date": "1970-01-01", "end_date": "2025-09-30" } } Response fields | Field | Type | Description | | ---- | ---- | ---- | | data | array | Open-text responses ordered by creation_date desc. | | data[].id / unique_id | string | Stable response identifier. | | data[].recipient | string | Email (when captured). | | data[].survey | string | Survey identifier. | | data[].creation_date | string | ISO timestamp of the response. | | data[].open_ended_response | string | Free-text answer content. | | data[].line_items | array | Associated products/items (if present). | | data[].line_items_count | integer | Number of items in line_items. | | data[].order_total | number | Monetary value associated with the response. | | data[].currency | string | Currency code. | | pagination | object | Pagination metadata supplied by the endpoint. | | pagination.total_count | integer | Total open-text responses matching the filters. | | pagination.page_size | integer | Page size applied to the request. | | pagination.current_page | integer | 1-indexed page number based on offset. | | pagination.total_pages | integer | Total calculated pages. | | pagination.has_next_page / has_previous_page | boolean | Convenience flags for pagination UI. | | filters | object | Effective filters included in the request. | | filters.survey_id | string | Survey identifier. | | filters.question_id | string | Question identifier. | | filters.start_date / end_date | string | Date range applied to the query. | 6. Raw Responses — GET /responses Provides tabular response data similar to the CSV export. Supports standard pagination and filtering. Sample request: curl "https://api.userloop.io/responses?survey_id=1710884429805x361876466030084100&start_date=2025-09-01&limit=25" \ -H "X-API-Key: " { "responses": [ { "id": "abc123", "unique_id": "abc123", "survey": "1710884429805x361876466030084100", "question_id": "1658...", "question_text": "How satisfied were you with your recent order?", "creation_date": "2025-09-10T12:00:00Z", "answer_id": "1658...", "answer_text": "5", "order_total": 99.99, "currency": "USD" } ], "pagination": { "total_count": 1500, "limit": 25, "offset": 0, "has_next_page": true } } Response fields | Field | Type | Description | | ---- | ---- | ---- | | responses | array | Tabular response data matching the export schema. | | responses[].id / unique_id | string | Response identifier. | | responses[].survey | string | Survey identifier. | | responses[].question_id | string | Question identifier. | | responses[].question_text | string | Question text captured at response time. | | responses[].creation_date | string | ISO timestamp of the response. | | responses[].answer_id | string | Answer identifier (if structured). | | responses[].answer_text | string | Selected answer text (or numeric/NPS value). | | responses[].open_ended_response | string | Free-text answer (when relevant). | | responses[].order_total | number | Order total associated with the response. | | responses[].currency | string | Currency code. | | responses[].utm_*, environment, surface, landing_site, etc. | string | Additional marketing and contextual metadata captured by UserLoop. | | pagination | object | Pagination metadata mirroring the request. | | pagination.total_count | integer | Total number of responses matching filters (may be null when exact count unavailable). | | pagination.limit | integer | Page size used for the query. | | pagination.offset | integer | Offset applied to the query. | | pagination.has_next_page | boolean | Indicates whether more pages are available. | 7. Single Response — GET /responses/{response_id} Retrieves one record by its unique ID. Useful when cross-referencing from webhooks or CRM. curl "https://api.userloop.io/responses/abc123" \ -H "X-API-Key: " { "response": { "id": "abc123", "unique_id": "abc123", "survey": "1710884429805x361876466030084100", "question_id": "1658...", "answer_text": "5" } } Response fields | Field | Type | Description | | ---- | ---- | ---- | | response | object | Response record matching the structure in the raw responses endpoint. | | response.id / unique_id | string | Response identifier. | | response.survey | string | Survey identifier. | | response.question_id | string | Question identifier. | | response.answer_text | string | Selected answer. Additional fields (e.g., order_total, utm_*) may be present depending on the record. | Error Handling Errors are returned with a consistent envelope: { "error": "Forbidden", "code": "FORBIDDEN", "detail": "Survey not allowed for this key" } Common error codes: - UNAUTHORIZED – Invalid, revoked, or expired key. - FORBIDDEN – Missing scope, survey not in allowlist, or origin not permitted. - BAD_REQUEST – Invalid parameters (missing IDs, malformed dates, etc.). - NOT_FOUND – Record does not exist or is not accessible to the caller. - INTERNAL – Upstream failure (e.g., Supabase error). Retry or contact support. All errors are safe to expose to clients; sensitive details (such as decrypted tokens) never appear in responses. Best Practices 1. Cache survey metadata when possible; the schema only changes when you update surveys in UserLoop. 2. Respect pagination limits. Use limit/offset for large exports. 3. Filter by date to speed up analytics calls, especially when embedding reporting dashboards. 4. Secure your keys. Store them in encrypted configuration stores and rotate periodically. 5. Monitor rate limits. Contact UserLoop if you expect sustained high throughput so we can tune allocations. Support If you encounter issues: 1. Confirm your key has the correct scopes and survey access inside the dashboard. 2. Double-check parameter spelling and formats (particularly survey_id, question_id, and date strings). 3. Review HTTP status codes and error payloads for hints. 4. Reach out to your UserLoop contact or support team with the request timestamp, key_id, and the full response body for faster troubleshooting. Happy building!

UserLoop SDK

The UserLoop JavaScript SDK lets you embed a survey or quiz in any website, including custom storefronts, landing pages, account areas, and post-purchase experiences. It has no framework dependency and renders a responsive survey inside an element you choose. Quick Start Add this snippet where you want the survey to appear. Replace YOUR_SURVEY_ID with the survey ID from your UserLoop dashboard. <div id="userloop_survey"></div> <script> (function () { var SURVEY_ID = 'YOUR_SURVEY_ID'; var TARGET_ID = 'userloop_survey'; function start() { var target = document.getElementById(TARGET_ID); if (!target) return; UserLoop(SURVEY_ID, target).init(); } if (window.UserLoop) { start(); return; } var script = document.createElement('script'); script.src = 'https://cdn.userloop.io/sdk-2/userloop.js'; script.async = true; script.onload = start; document.head.appendChild(script); })(); </script> The SDK loads the published survey, applies its colours and settings, and handles navigation and response submission. A single-select survey rendered by the UserLoop JavaScript SDK Initialising the SDK If your site manages script loading itself, include the SDK and create an instance directly: <script src="https://cdn.userloop.io/sdk-2/userloop.js"></script> <div id="userloop_survey"></div> <script> const survey = UserLoop( 'YOUR_SURVEY_ID', document.getElementById('userloop_survey'), { email_collection: true, expanded_mode: false, }, { customer: {}, transaction: {}, } ); survey.init(); </script> The initializer is: UserLoop(surveyId, targetElement, config, context) | Argument | Description | |---|---| | surveyId | The survey ID shown in UserLoop. | | targetElement | The DOM element in which the survey should render. | | config | Optional SDK behaviour and preview settings. | | context | Optional customer and transaction data to attach to responses and use for targeting. | Call init() after the target element exists in the page. Common Configuration Options | Option | Type | Description | |---|---|---| | email_collection | boolean | Shows an email field when the survey requires one and the supplied customer has no email address. | | expanded_mode | boolean | Expands the survey to the width of its container. | | preview_mode | boolean | Renders the survey for design and testing without applying live question targeting. | | preview_start_question_id | string | Opens a preview on a particular question. | | preview_thankyou_page | boolean | Opens the thank-you screen directly in preview mode. | | auto_answer_first_question | string | Selects and submits a particular answer ID on the first question. Useful for links from email campaigns. | | allow_restart | boolean | Adds an option to restart the survey after completion. | | surface | string | Identifies a custom integration surface in response metadata. The default is sdk. | | quiz_mode | boolean | Enables quiz behaviour for a survey configured as a quiz. | Branding, colours, progress bars, button text, thank-you content, rewards, and most presentation settings come from the published survey configuration rather than the embed code. Instance Methods UserLoop(...) returns an instance with these methods: | Method | Description | |---|---| | init() | Loads and renders the survey. Returns a promise. | | refresh() | Reloads the current survey configuration. | | refresh(newSurveyId) | Switches the instance to another survey and loads it. | | setSurveyId(newSurveyId) | Alias for switching surveys with refresh(newSurveyId). | | mount() | Mounts the survey into its configured target element. | Supported Question Types The SDK renders the question types configured in UserLoop, including Single Select, Multi Select, Open Ended, Email, Date, Phone, Number Score and NPS, CSAT, Video, and Info Screen. Follow-up questions, optional questions, custom button text, auto-proceed, redirect links, progress indicators, and thank-you rewards are driven by the survey configuration. See Question Types for guidance on choosing and configuring questions. Info Screens An Info Screen is a content-only step. Use one to introduce a survey, explain what will happen next, divide a longer survey into sections, or describe a reward before asking a question. An Info Screen with a heading, image, explanatory copy, and Continue button in the UserLoop SDK An Info Screen can contain: - A heading - Multi-line supporting text - An optional image - The survey's normal Continue button It is placed in the survey like any other question, so you can position it at the beginning, end, or between questions. The same product and customer targeting rules also apply. For API-driven survey configurations, an Info Screen has type: "Info Screen": { "question_id": "intro-screen", "type": "Info Screen", "question": "Before you start", "question_subheading": "This takes about two minutes.\n\nYou'll receive 10% off at the end.", "info_image": "https://example.com/survey-intro.jpg", "required": false, "answers": [] } Info Screen behaviour: - It collects and submits no answer, so it creates no response row and does not appear in response exports. - It advances the progress bar as a survey step, but is not numbered as a question. - Supporting text keeps its line breaks. - A broken optional image is removed without preventing the rest of the screen from loading. - The Continue button is always available. Required and skip settings do not apply. - An Info Screen cannot be used as a conditional follow-up question. - If email collection is enabled, the email field appears on the first answerable question rather than on the Info Screen. - A live survey containing only Info Screens is not shown because there is no response to collect. Preview mode can still render it for design review. Info Screens are supported by the JavaScript SDK and by UserLoop's Shopify Checkout, Popup Survey, and App Block survey experiences. Passing Customer and Transaction Data Pass customer and order context as the fourth argument. All fields are optional; send only the data your site has. const customer = { id: 'customer_123', email: '[email protected]', firstName: 'Alex', lastName: 'Morgan', order_count: 3, }; const transaction = { transaction_id: 'order_456', order_creation_date: '2026-08-07T09:30:00.000Z', total: 89.95, currency: 'GBP', coupon_code: 'WELCOME10', platform: 'Custom Storefront', source: 'website', utm_source: 'newsletter', products: [ { id: 'shopify-product-id', name: 'Example product', product_url: 'https://example.com/products/example-product', }, ], }; const survey = UserLoop( 'YOUR_SURVEY_ID', document.getElementById('userloop_survey'), { email_collection: true }, { customer, transaction } ); await survey.init(); This context enriches the response data and lets UserLoop decide whether targeted questions apply. Question Targeting Questions, including Info Screens, can be limited by product or customer status in the survey configuration: - Product targeting matches against product IDs in the transaction. For Shopify, pass product IDs rather than variant IDs. - New-customer and returning-customer targeting uses customer.order_count or transaction.order_count. - Live embeds hide questions whose targeting rules do not match. - Preview mode intentionally ignores targeting and renders every configured question so the complete design can be reviewed. If targeting removes every answerable question, the live survey container is hidden instead of displaying an empty survey. Refreshing or Switching Surveys Use refresh() when your page gains updated order data or when you want to reload the published survey configuration: await survey.refresh(); To switch the same embed to another survey: await survey.refresh('ANOTHER_SURVEY_ID'); // Equivalent: await survey.setSurveyId('ANOTHER_SURVEY_ID'); Video Responses Video questions let respondents record with their camera or choose an existing file. The SDK handles permission prompts, upload progress, retry messaging, and the normal response submission flow. Whether Video is available and its maximum duration are controlled by the survey configuration. Because camera and microphone access require a secure browser context, serve pages containing Video questions over HTTPS. If a respondent declines camera access, the file-upload option can still be used where supported. Integrations When the survey is connected to integrations such as Shopify, Klaviyo, Slack, gift cards, webhooks, or response notifications, the published survey includes the necessary integration flags. The SDK copies those flags into response payloads automatically; you do not need to duplicate them in your embed configuration. Testing and Troubleshooting Before publishing your integration: 1. Confirm the survey is published and the survey ID is correct. 2. Confirm https://cdn.userloop.io/sdk-2/userloop.js is allowed by your site's Content Security Policy. 3. Make sure the target element exists before calling UserLoop(...). 4. Test at mobile and desktop widths. 5. Pass representative product IDs and order counts when testing targeted questions. 6. Use preview_mode: true to review all questions, including those whose live targeting would not match your test customer. 7. Check the browser console and Network panel if the survey does not load. Do not place private credentials or API keys in browser-side SDK configuration. The survey ID is intended to be used by the browser; secret server-side keys are not. Need Help? If you have questions about an SDK integration, contact us through live chat. Include the page URL, survey ID, browser-console error, and a description of the behaviour you expected so we can investigate quickly.

MCP Server

The UserLoop MCP server connects Claude, ChatGPT, and other compatible AI clients to your UserLoop survey data through the Model Context Protocol. You can explore feedback, compare answer counts, review NPS, find individual responses, and chart response volume by asking questions in natural language. All UserLoop MCP tools are read-only. They can retrieve and analyse survey data, but they cannot create, edit, or delete anything in your UserLoop account. What You Can Ask Try prompts such as: - “Chart response volume across all surveys for the last 90 days and break it down by survey.” - “How did customers answer ‘How did you hear about us?’ this month?” - “Show the NPS score and promoter, passive, and detractor breakdown for our quarterly survey.” - “Summarise the main themes in open-text responses from the post-purchase survey.” - “Compare answer counts, revenue, and average order value for this question.” Your AI client chooses the appropriate UserLoop tools automatically. Interactive Charts Clients that support MCP Apps can display UserLoop results as interactive charts directly in the conversation. Clients without MCP Apps support still receive the same data as compact JSON. Response Volume Over Time View response volume for one survey, one question, or every survey combined. Switch between 7 days, 30 days, 90 days, 12 months, or all time, and use automatic, daily, weekly, or monthly grouping. Company-wide results also include a per-survey breakdown and a data-table view. UserLoop MCP response-volume chart showing weekly responses across all surveys Answer Counts Multiple-choice results are shown as a horizontal bar chart with response counts and share. Hover over a result to see more detail, including revenue and average order value when available. UserLoop MCP answer-count chart for a multiple-choice survey question NPS Number Score and NPS questions include the headline NPS score, average score, promoter/passive/detractor split, 0–10 score distribution, and revenue metrics when available. UserLoop MCP NPS chart showing score and promoter, passive, and detractor segments The screenshots above use example data. When connected, the charts use your UserLoop survey data. Available Tools The server currently provides eight tools: | Tool | What it does | |---|---| | userloop_health | Checks the connection and confirms your API key is valid | | userloop_list_surveys | Lists all surveys, including their questions and question types | | userloop_get_survey | Retrieves one survey and its questions | | userloop_analytics_counts | Returns answer counts for a question, including NPS and revenue metrics when available | | userloop_responses_timeseries | Charts response volume by day, week, or month for one survey, one question, or all surveys | | userloop_responses_open | Retrieves paginated open-text responses for a question | | userloop_responses_raw | Retrieves paginated raw responses for a survey | | userloop_get_response | Retrieves one response by its ID | Before You Connect Create a UserLoop API key in your UserLoop account. API keys start with ul_live and are shown only once, so create a new one if you no longer have the full key. See UserLoop API for more information. Your MCP server URL is: https://mcp.userloop.io/mcp?api_key=ul_live_... Replace ul_live_... with your complete API key. Keep this URL private. It contains your UserLoop API key. Do not share it, publish it, or include it in screenshots. Rotate the key immediately if it is exposed. Connect to Claude or Claude Desktop Remote MCP connectors are available on supported Claude plans. 1. Open Settings > Connectors in Claude or Claude Desktop. 2. Select Add custom connector. 3. Name the connector UserLoop. 4. Paste the complete MCP server URL, including ?api_key=.... 5. Select Add. 6. In a conversation, open Search and tools and enable the UserLoop connector. Remote servers in Claude Desktop are configured through Settings > Connectors, not claude_desktop_config.json. See Anthropic’s custom connector guide for current plan and workspace requirements. Connect to ChatGPT Custom MCP apps are available to supported ChatGPT workspace plans and may need to be enabled by a workspace administrator. 1. Enable developer mode if your workspace requires it. 2. Open Settings > Apps > Create, or ask a workspace administrator to open Workspace settings > Apps > Create. 3. Enter the complete MCP server URL, including ?api_key=.... 4. Choose No authentication if prompted. The UserLoop API key is already included in the URL you entered. 5. Scan the available tools and create the app. 6. Enable UserLoop from the app menu in a conversation. See OpenAI’s developer mode and MCP apps guide for current availability and administrator controls. Connect from Another MCP Client Use the complete URL above with a client that supports remote MCP servers over Streamable HTTP. A typical configuration looks like this: { "mcpServers": { "userloop": { "type": "http", "url": "https://mcp.userloop.io/mcp?api_key=ul_live_..." } } } The exact configuration format depends on your MCP client. Privacy and Email Redaction Email redaction is enabled by default for open-text, raw-response, and individual-response tools. For example, [email protected] is returned in a masked form. This helps protect customer privacy when data is sent to a third-party AI service. The response tools support redact_emails: false, but only disable redaction when you have a clear reason and are comfortable sharing those addresses with your AI provider. Response-Volume Notes - Response-volume charts can combine all surveys in a single request and include totals for each survey. - A response-volume row represents an answer given to a question, rather than a completed survey submission. Use a question filter when you want to count answers to one specific question. - The server reads up to 2,000 response rows for a time-series request. If a result is marked as truncated, narrow the date range or select a specific survey. Troubleshooting The connection or health check fails - Confirm that the URL includes the complete ?api_key=ul_live_... query parameter. - The MCP server no longer accepts the old X-UserLoop-Key header method. - Create a new UserLoop API key if the original key has been revoked, lost, or exposed. The new time-series tool is missing Refresh or rescan the connector’s tools. In ChatGPT, newly added tools may need to be enabled by a workspace administrator. If refreshing does not work, remove and re-add the connector using the current URL. Charts are not displayed Your client may not support MCP Apps, or it may need to refresh the UserLoop connector. The tool still returns the underlying data, which the AI assistant can analyse and present as text or a table. Need Help? If you have questions about the UserLoop MCP server, contact us through live chat and we’ll be happy to help.