---

## Getting Started

* **Base URL:** 

```
https://api.userloop.io
```

* **Generate API keys** inside the [UserLoop dashboard](https://userloop.io/home?tab=Account). 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: 

```bash
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. 

```json
{ "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:

```bash
curl "https://api.userloop.io/surveys" \ -H "X-API-Key: "
```

Example response (trimmed for brevity): 

```json
{ "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. 

```bash
curl "https://api.userloop.io/surveys/1710884429805x361876466030084100" \ -H "X-API-Key: "
```


```json
{ "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:

```bash
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): 

```json
{ "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: 

```bash
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): 

```json
{ "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: 

```bash
curl "https://api.userloop.io/responses?survey_id=1710884429805x361876466030084100&start_date=2025-09-01&limit=25" \ -H "X-API-Key: "
```


```json
{ "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. 

```bash
curl "https://api.userloop.io/responses/abc123" \ -H "X-API-Key: "
```


```json
{ "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: 

```json
{ "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!