Articles on: Developer Documentation

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 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: <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

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: <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

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: <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

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: <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

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: <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

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: <your_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!


Updated on: 08/10/2025

Was this article helpful?

Share your feedback

Cancel

Thank you!