UserLoop API
UserLoop Public API Guide (v1)
Welcome! This document explains how to integrate with the UserLoop Public API. The API exposes survey metadata, aggregated analytics, and raw survey responses.
If you are just getting started, read through the "Getting Started" section and then explore the endpoint reference for concrete payload examples.
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: <your_api_key>"
The API validates scopes, survey allowlists, and optional origin restrictions before serving any protected data.
Date & Pagination Helpers
start_date
andend_date
must useYYYY-MM-DD
format. When omitted, endpoints default to the broadest safe range (from1970-01-01
through the current day) so analytics always receive explicit boundaries.limit
andoffset
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 |
---|---|---|
| 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: <your_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 |
---|---|---|
| string | Company ID |
| integer | Number of surveys returned. |
| array | Collection of survey summaries ordered by last update. |
| string | Unique survey identifier. |
| string | Human readable survey name. |
| string | Channel (Email, Checkout, Link, etc.). |
| string | |
| integer | Number of questions associated with the survey. |
| array | List of question IDs. |
| ISO 8601 string | Creation and last modification timestamps. |
| object | Grouped metadata copied from the survey configuration. Keys are normalized to |
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: <your_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 |
---|---|---|
| string | Survey identifier. |
| string | Survey title. |
| string | Owning company. |
| array | Ordered list of questions in the survey. |
| string | Question identifier used in analytics queries. |
| string | Question text. |
| string | Question type (CSAT, NPS, etc.). |
| array | Answer options (when applicable). |
| string | Answer identifier used in analytics filters. |
| 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 |
---|---|---|
| string | Survey to analyze. Must be enabled for the calling key. |
| string | Question to aggregate. |
Optional query parameters:
Name | Type | Description |
---|---|---|
| comma-separated strings | Restrict analytics to specific answer IDs. Defaults to all answers in the question. |
| | 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: <your_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 |
---|---|---|
| array | Aggregated metrics per answer option (sorted by count desc). |
| string | Answer identifier. |
| string | Answer label (resolved from survey config when available). |
| integer | Number of responses recorded for the answer. |
| number | Share of total responses for the answer (0–100). |
| integer | Sum of |
| integer | Average order value for the answer (rounded). |
| string | Currency code used for revenue metrics. |
| object | Contextual metadata for the aggregation. |
| string | Survey identifier. |
| string | Survey title (if available). |
| object | Question metadata ( |
| integer | Count of responses returned by analytics ( |
| integer | Unique respondent count. |
| integer | Sum of |
| object | Effective filters applied to the analytics call. |
| array | Answer IDs used for aggregation. |
| 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: <your_api_key>"
Example response (first record shown):
{
"data": [
{
"id": "abc123",
"unique_id": "abc123",
"recipient": "customer@email.com",
"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 |
---|---|---|
| array | Open-text responses ordered by |
| string | Stable response identifier. |
| string | Email (when captured). |
| string | Survey identifier. |
| string | ISO timestamp of the response. |
| string | Free-text answer content. |
| array | Associated products/items (if present). |
| integer | Number of items in |
| number | Monetary value associated with the response. |
| string | Currency code. |
| object | Pagination metadata supplied by the endpoint. |
| integer | Total open-text responses matching the filters. |
| integer | Page size applied to the request. |
| integer | 1-indexed page number based on |
| integer | Total calculated pages. |
| boolean | Convenience flags for pagination UI. |
| object | Effective filters included in the request. |
| string | Survey identifier. |
| string | Question identifier. |
| 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: <your_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 |
---|---|---|
| array | Tabular response data matching the export schema. |
| string | Response identifier. |
| string | Survey identifier. |
| string | Question identifier. |
| string | Question text captured at response time. |
| string | ISO timestamp of the response. |
| string | Answer identifier (if structured). |
| string | Selected answer text (or numeric/NPS value). |
| string | Free-text answer (when relevant). |
| number | Order total associated with the response. |
| string | Currency code. |
| string | Additional marketing and contextual metadata captured by UserLoop. |
| object | Pagination metadata mirroring the request. |
| integer | Total number of responses matching filters (may be |
| integer | Page size used for the query. |
| integer | Offset applied to the query. |
| 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: <your_api_key>"
{
"response": {
"id": "abc123",
"unique_id": "abc123",
"survey": "1710884429805x361876466030084100",
"question_id": "1658...",
"answer_text": "5"
}
}
Response fields
Field | Type | Description |
---|---|---|
| object | Response record matching the structure in the raw responses endpoint. |
| string | Response identifier. |
| string | Survey identifier. |
| string | Question identifier. |
| string | Selected answer. Additional fields (e.g., |
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
- Cache survey metadata when possible; the schema only changes when you update surveys in UserLoop.
- Respect pagination limits. Use
limit
/offset
for large exports. - Filter by date to speed up analytics calls, especially when embedding reporting dashboards.
- Secure your keys. Store them in encrypted configuration stores and rotate periodically.
- Monitor rate limits. Contact UserLoop if you expect sustained high throughput so we can tune allocations.
Support
If you encounter issues:
- Confirm your key has the correct scopes and survey access inside the dashboard.
- Double-check parameter spelling and formats (particularly
survey_id
,question_id
, and date strings). - Review HTTP status codes and error payloads for hints.
- 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!
Updated on: 08/10/2025
Thank you!