> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gleap.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Surveys REST API

> Create and validate survey drafts, publish, read stats, responses and AI insights, reach out to respondents and export responses.

All survey endpoints live under the [admin REST API](/documentation/server/api-overview) and use its authentication:

```
Authorization: Bearer YOUR_API_KEY
Project: YOUR_PROJECT_ID
```

The API host depends on the [data region](/documentation/guides/data-regions) of your project:

| Data region | API host |
| - | - |
| EU (default) | `https://api.gleap.io` |
| US | `https://api.us.gleap.ai` |

<Info>
  The examples on this page use the EU host. If your project lives in another region, replace `https://api.gleap.io` with your region's host. API tokens are per project and only valid in the project's region.
</Info>

`{id}` below is the survey's Gleap id (the `_id` returned by `GET /v3/engagement/surveys`). The id you pass to `Gleap.showSurvey` is the survey's SDK id (`actionType`) — a different value.

Every endpoint is project-scoped and checks the same survey permissions as the dashboard (viewers can read, members and admins can edit).

## Surveys

| Method | Path | |
| - | - | - |
| `GET` | `/v3/engagement/surveys` | List surveys (`limit`, `skip`, field filters such as `status=live`). |
| `GET` | `/v3/engagement/surveys/{id}` | One survey: `survey.draft`, `survey.published`, `survey.version`, `survey.page`, targeting. |
| `POST` | `/v3/engagement/surveys` | Create a survey as a draft. |
| `PUT` | `/v3/engagement/surveys/{id}/draft` | Save the draft definition. |
| `POST` | `/v3/engagement/surveys/{id}/validate` | Validate a definition without saving. |
| `POST` | `/v3/engagement/surveys/{id}/kai-edit` | Ask Kai to edit a definition: `{ instruction, definition?, selectedId?, lang? }` → `{ status, summary, definition, changes, warnings }` for review. Saves nothing; included in paid plans (no AI credits). |
| `POST` | `/v3/engagement/surveys/{id}/publish` | Publish the draft as the next version. |
| `GET` | `/v3/engagement/surveys/{id}/versions` | Published versions. |
| `PUT` | `/v3/engagement/surveys/{id}/page` | Survey page settings. |
| `DELETE` | `/v3/engagement/surveys/{id}` | Delete the survey with its versions and **all of its responses**. |

### Delete a survey

`DELETE /v3/engagement/surveys/{id}` deletes the survey at once. Everything stored for it follows in the background, usually within a minute and at the latest after about ten: every response (including the files respondents uploaded, and the survey tickets of a survey created before Surveys 2.0), its published versions, stats, analytics events and search entries. Conversations started from a response stay (without the link to it). Live email campaigns that send the survey are paused right away (their pending sends are dropped) and kept, without the survey. `GET /v3/engagement/surveys/{id}/delete-preview` returns what a deletion affects besides the survey's own data: `{ liveEmailCampaigns: [{ id, name }], legacyTickets }`. This cannot be undone: export the responses first if you need them.

### Create a draft

```bash theme={null}
curl -X POST https://api.gleap.io/v3/engagement/surveys \
  -H "Authorization: Bearer $GLEAP_API_KEY" -H "Project: $GLEAP_PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Post-onboarding NPS",
    "format": "card",
    "definition": { "schemaVersion": 2, "format": "card", "defaultLanguage": "en", "blocks": [ … ], "endings": [ … ], "fields": [], "showProgress": true, "allowSkipBack": true }
  }'
```

The definition format is described in [Survey definition](/documentation/surveys/definition). New surveys are always drafts; nothing is shown to anyone until the survey is published and set live.

### Save the draft

```bash theme={null}
curl -X PUT https://api.gleap.io/v3/engagement/surveys/$SURVEY/draft \
  -H "Authorization: Bearer $GLEAP_API_KEY" -H "Project: $GLEAP_PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{ "definition": { … } }'
```

Returns `{ definition, warnings }`. Drafts may contain invalid logic — `warnings` lists what publishing would reject. Saving the draft of a live survey doesn't change what respondents see.

### Validate

`POST /validate` with `{ "definition": { … } }` returns `{ ok, errors: [{ path, code, message }] }` and saves nothing.

### Publish

`POST /publish` validates the draft, stores it as the next immutable version and makes it the version respondents see. Invalid drafts return `422` with `errors`. Publishing doesn't start the survey in your app: that is the survey's `status` (`live` shows it to the in-app audience, **Share → In the app** in the dashboard), or send `{ "goLive": true }` to publish and start it in one call. The survey page and emails use the published version whatever the status.

<Note>
  AI agents (the [Gleap MCP server](/documentation/surveys/mcp)) publish through the same endpoint, with the role of the teammate who connected them.
</Note>

### Survey page settings

`PUT /page` takes:

```json theme={null}
{
  "slug": "onboarding",
  "enabled": true,
  "access": "anyone",
  "onePerPerson": true,
  "closeAt": "2026-12-31T23:59:59.000Z",
  "maxResponses": 500,
  "afterSubmit": { "mode": "redirect", "url": "https://yourcompany.com/thanks", "delaySeconds": 3 }
}
```

`slug` is unique per project (`^[a-z0-9-]{2,60}$`). `access` is `anyone` or `personal_links_only`. `afterSubmit.mode` is `ending` or `redirect`.

## Results

### Stats

`GET /v3/engagement/surveys/{id}/stats?from=2026-09-01&to=2026-09-30&version=3`

Computed from stored counters, so it is cheap at any volume. `nps` (score and distribution) is present when the survey has an NPS question; `perBlock` is keyed by answer key, `edges` counts the paths between blocks (`from>to`).

```json theme={null}
{
  "totals": { "shown": 4210, "started": 1630, "completed": 1288, "partial": 214, "medianOrAvgDurationMs": 38000 },
  "nps": { … },
  "perBlock": { "nps_score": { "reached": 1630, "answered": 1601, "skipped": 0, "values": { "0": 31, "9": 288, "10": 322 } } },
  "edges": { "q_nps>q_detractor": 276 },
  "endings": { "end_thanks": 1288 },
  "channels": { "web": 980, "ios": 210, "email": 98 },
  "daily": [ … ]
}
```

### Analytics

`GET /v3/engagement/surveys/{id}/analytics?from=2026-09-01&to=2026-09-30` — views, starts, submissions, the question funnel with drop-off, timing, breakdowns and the previous period for comparison, from survey events. See [Survey analytics](/documentation/surveys/analytics#api).

### Responses

`GET /v3/engagement/surveys/{id}/responses`

| Query | |
| - | - |
| `status` | `completed`, `partial`, `in_progress` (comma-separated for several) |
| `filter[<answer key>]` | Answer filter, e.g. `filter[cancel_reason]=too_expensive` or a numeric range `filter[nps_score]=0..6` |
| `theme` | AI theme id |
| `channel` | `web`, `ios`, `android`, `page`, `email`, `embed`, `conversation`, `api` |
| `q` | Text search in text answers and the respondent's name or email |
| `field[<hidden field key>]` | Exact match on a hidden field, e.g. `field[plan]=pro` (up to 10) |
| `device` | `phone`, `tablet`, `desktop` |
| `quality` | `speeder` or `straight_line` — see [Analytics → Response filters](/documentation/surveys/analytics#response-filters-and-export) |
| `limit` | Page size |
| `cursor` | `nextCursor` of the previous page |

Returns `{ responses: [...], nextCursor }`, newest first. Response objects are described in [Survey definition → Responses](/documentation/surveys/definition#responses).

<Note>
  **Compatibility for existing pollers.** Integrations that page through this endpoint with `skip` / `limit` keep receiving the previous shape — an array of `{ _id, formData, session, createdAt, updatedAt }`, where `formData` holds the answers keyed by answer key (for a survey created before Surveys 2.0: by its old field names). Switch to `cursor` to get the new fields (status, path, version, channel, themes).
</Note>

| Method | Path | |
| - | - | - |
| `GET` | `/responses/{responseId}` | One response with a contact summary and the definition of the version it answered. |
| `DELETE` | `/responses/{responseId}` | Delete a response. |
| `GET` | `/responses/export?format=csv` | Stream all responses as CSV (one column per answer key, plus timing, device, source and hidden-field columns) or `format=json`. Takes the same filters. A survey created before Surveys 2.0 also exports `ticketId`, `bugId`, `triggerEvent`, `triggerDate`, `triggerEventData`, `customData`, `metaData` and `legacyAnswers` (answers to questions deleted before the upgrade) for its earlier answers. |

### Survey answers on the ticket API (until January 5, 2027)

Before Surveys 2.0 every answer was a ticket of type `SURVEY`. Until **January 5, 2027** the ticket API keeps returning them, without creating tickets:

* `GET /v3/tickets?type=SURVEY`, `GET /v3/tickets/export?type=SURVEY` and `GET /v3/tickets/csv-export?type=SURVEY` list survey answers in the ticket shape, newest first (`skip` / `limit`, `outbound`, `session` and date filters work). An answer from before the upgrade is its original ticket (same `_id`, `bugId`, `formData`); a newer one is a ticket view with `_id` = the response id, `formData` under the old field names (choices as their text), the contact's fields in `session`, `createdAt` = when it was completed, and `surveyResponse`. Merged pages go up to 25,000 rows deep.
* `GET /v3/sessions/{sessionId}/activities?surveyResponses=include` adds the contact's survey responses to its tickets; `surveyResponses=only` lists only them (with `surveyName`, `outbound` and `responseStatus`).

From January 5, 2027 `type=SURVEY` returns tickets only. Move to `GET /v3/engagement/surveys/{id}/responses`, the [survey webhooks](/documentation/surveys/webhooks) or [automations](/documentation/surveys/automations) before then.

### Reach out to a respondent

`POST /v3/engagement/surveys/{id}/responses/{responseId}/reach-out`

```json theme={null}
{ "channel": "messenger", "message": "Thanks for the honest feedback — can we help with the sync issue?", "assignToMe": true }
```

`channel` is `messenger` or `email` (with an optional `subject`); `message` is plain text or a TipTap document. Gleap starts a normal conversation with the contact, links it to the response and returns `{ ticketId, ticketShareToken }`.

## AI insights

| Method | Path | |
| - | - | - |
| `GET` | `/insights` | Stored insights: `themes`, `summary`, `drivers`, `segments`, `alerts`, `updatedAt`, `generation`. |
| `POST` | `/insights/refresh` | Rebuild the insights now (at most every 5 minutes and 3 times a day per survey). Returns `{ queued, refreshesLeft?, retryAfterSeconds? }`. |
| `POST` | `/insights/ask` | Ask a question about the responses. |
| `PUT` | `/themes` | Rename, merge or delete themes. |
| `POST` | `/insights/links` | Link a bug or feature request (`ticketId`) or a Kai Code session (`cloudSessionId`) to a theme (`themeId`) and/or a finding (`finding`: its title). The ticket or session must belong to the same project. Returns `{ link }`. |
| `DELETE` | `/insights/links/{linkId}` | Remove a link (the ticket or session stays). |

`GET /insights` returns the links as `links` and per theme as `themes[].links`, each with the ticket's number, title, status and type or the Kai Code session's title and status. Links to deleted tickets or sessions are left out.

Insights are generated on demand: `GET /insights` always answers at once with what is stored. When responses came in since the last update and that update is more than 5 minutes old, the read also starts one update in the background (tagging new answers with themes, recounting, and rewriting the summary after 25 newly tagged answers or 10% more responses). `generation` tells you its state:

```json theme={null}
{ "status": "updating", "capped": false, "resumesAt": null, "refreshesLeft": 3, "freshMinutes": 5, "skipped": null }
```

Read again in a minute or two while `status` is `updating`. Without new responses, reading the insights uses no AI. Each project has a daily budget for survey AI (2,000 answers tagged and 20 summaries or theme lists a day); when it is used up, `capped` is `true` and updates resume at `resumesAt` (the next midnight UTC). A survey nobody opens is never analyzed.

`POST /insights/ask`:

```json theme={null}
// request
{ "question": "Why did NPS drop this week?" }
// response
{
  "answer": "Mentions of **sync reliability** doubled to 31, all from Slack workspaces since Sep 29 …",
  "citations": [{ "responseId": "rsp_4kq9X2mT7vB1nR8sLp3Z", "quote": "Slack sync stopped twice this week" }],
  "basedOn": 412
}
```

AI usage (themes, insights, ask) is billed as [token usage](/documentation/guides/ai-pricing-and-credits).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.