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

# Surveys end to end: distribute, remind non-responders, results, benchmark

> Run a NPS / CSAT / CES survey over the API — author the template, distribute to a contact, list, or segment, re-contact the laggards, aggregate the results, benchmark against your industry vertical, and mine the verbatims with Voice-of-Customer analytics.

# Surveys end to end

A survey is more than a message. This guide follows the full feedback cycle over the surveys API: author a template, distribute it, chase the non-responders, read the aggregate scores, compare them against published industry benchmarks, and group the free-text verbatims into Voice-of-Customer (VoC) themes. For request/response schemas see the [surveys API reference](/api-reference/endpoints/surveys).

## 1. Author the survey template

Create the template with `POST /api/v1/surveys`. The key fields:

| Field                             | Notes                                                                                                                                              |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                            | `nps`, `csat`, or `ces`. Picks the scoring formula for the results and benchmark endpoints.                                                        |
| `question`                        | The prompt sent to the customer (up to 500 chars).                                                                                                 |
| `channels`                        | The channels allowed for sends: any of `sms`, `whatsapp`, `email`, `viber`, `rcs`, `push`, up to 6. A send can only fire on a channel listed here. |
| `follow_up_prompt`                | Optional second line of copy shown after the question (e.g. "Tell us why in one sentence").                                                        |
| `questions`                       | Optional multi-question definition with choice / rating / matrix / open-text types and branch rules. Omit for a single transactional question.     |
| `translations` / `default_locale` | Optional per-locale content variants keyed by BCP-47 tag. A recipient's contact language selects the best variant at delivery time.                |

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/surveys" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Post-campaign CSAT",
    "type": "csat",
    "question": "How satisfied were you with your recent interaction? (0–10)",
    "channels": ["sms", "email"],
    "follow_up_prompt": "Tell us why in one sentence."
  }'
```

The response is the saved template with its `id` — keep it for every call below. A single-question NPS template answers with any number 0–10; a `csat` or `ces` template uses a weighted mean of those scores. Multi-question surveys carry an `answers` array per response (see the API reference).

## 2. Distribute

`POST /api/v1/surveys/{id}/send` fans the survey out to the audience you supply — one of `contact_id`, `contact_ids` (up to 1,000), or `segment_id`, resolved against contacts at send time. Channels: the same `channel` you intend (must be one the template lists).

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/surveys/$SURVEY_ID/send" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "segment_id": "seg_recent_customers"
  }'
```

Each recipient gets a signed landing link plus (on phone channels) a short 4-character reply token embedded as `[#A7Z2]` in the message body, so a bare numeric reply binds to the right survey even when you run several at once. Two frequency-cap layers protect the contact: a fixed 24-hour same-survey dedupe, and a configurable cross-survey fatigue window (default 72 hours, overridable per send with `fatigue_window_hours`; pass `0` to disable). The response reports `requested`, `sent`, and `skipped` counts; an empty audience returns `{ "requested": 0, "sent": 0, "skipped": 0, "reason": "empty_audience" }` rather than an error. Per send, the audience is capped at a 5,000-contact blast.

**Response:**

```json theme={null}
{
  "data": {
    "requested": 482,
    "sent": 471,
    "skipped": 11,
    "skipped_fatigue": 6,
    "fatigue_window_hours": 72
  }
}
```

`skipped` includes recipients with no address on the chosen channel and recipients inside either dedupe window, so compare `requested` vs `sent` before assuming a problem.

## 3. Re-contact the laggards

Response rate depends on a follow-up wave. Two endpoints form the remind loop:

**List the non-responders.** `GET /api/v1/surveys/{id}/non-responders` returns contacts whose most recent send for this survey is older than `stale_hours` (default 24) with no response recorded. Paginate with `limit`/`offset`.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/surveys/$SURVEY_ID/non-responders?stale_hours=24&limit=200" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

**Response:**

```json theme={null}
{
  "data": {
    "contacts": [
      {
        "contact_id": "cnt_9f2c…",
        "channel": "sms",
        "sent_at": "2026-08-24T09:12:00Z",
        "first_name": "Mara",
        "last_name": "Kovacs",
        "phone": "+13125550112",
        "email": null
      }
    ],
    "total": 47,
    "stale_hours": 24
  }
}
```

**Fire the reminder wave.** `POST /api/v1/surveys/{id}/remind` re-dispatches those non-responders on the channel each one was originally sent through. Optionally narrow with `contact_ids` (a subset of the non-responders list, up to 1,000), tune `stale_hours`, or override the fatigue window. Each dispatched contact passes the same channel check, dedupe, and fatigue gate as a fresh send, so a reminder cannot spam someone re-surveyed by another process.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/surveys/$SURVEY_ID/remind" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"stale_hours": 24}'
```

**Response:**

```json theme={null}
{
  "data": {
    "requested": 47,
    "sent": 41,
    "skipped": 6,
    "skipped_by_reason": { "deduped": 4, "no_recipient": 2 }
  }
}
```

Reminders go out on the channel the contact originally received the survey on — the same-channel contract matters for SMS reply matching.

## 4. Aggregate the results

`GET /api/v1/surveys/{id}/results` rolls up the survey: totals, response rate, average score, the NPS computation (for `nps` type surveys), and the score breakdown a bar chart renders.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/surveys/$SURVEY_ID/results" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

**Response:**

```json theme={null}
{
  "data": {
    "total_sent": 529,
    "total_responded": 213,
    "response_rate": 0.4026,
    "avg_score": 7.85,
    "nps_score": 33.5,
    "breakdown_by_score": [
      { "score": 0, "count": 4 },
      { "score": 5, "count": 11 },
      { "score": 7, "count": 38 },
      { "score": 8, "count": 51 },
      { "score": 9, "count": 47 },
      { "score": 10, "count": 62 }
    ]
  }
}
```

`avg_score` is the weighted mean of scored responses (always populated on any type); `nps_score` (promoters minus detractors in the −100…100 scale) is `null` for non-NPS surveys. `response_rate` is responded / sent, rounded to four decimals.

## 5. Compare against the benchmark

`GET /api/v1/surveys/{id}/benchmark` tells you whether the score is good, not just what it is. It maps the same breakdown (weighted mean for `csat`/`ces`, NPS formula for `nps`) onto published peer distributions — Retently / Qualtrics / ACSI-class bands — and returns an interpolated percentile rank (1–99), a quartile rating, and the signed delta vs the industry median.

Pick the peer group with `?vertical=` — one of `all` (the default), `saas`, `ecommerce`, `retail`, `financial_services`, `insurance`, `telecom`, `healthcare`, `hospitality`, `education`.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/surveys/$SURVEY_ID/benchmark?vertical=saas" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

**Response:**

```json theme={null}
{
  "data": {
    "metric": "nps",
    "vertical": "saas",
    "vertical_label": "SaaS / Software",
    "value": 33.5,
    "unit": "points",
    "sample_size": 213,
    "min_sample_size": 30,
    "percentile": 47.9,
    "rating": "below_average",
    "benchmark": { "p10": 0, "p25": 20, "median": 36, "p75": 52, "p90": 66 },
    "delta_vs_median": -2.5,
    "summary": "Your NPS of 33.5 places you in the 47.9th percentile among SaaS / Software peers — below the industry median of 36 by 2.5 points."
  }
}
```

A low sample still echoes your measured value and the bands, but the percentile is suppressed and `rating` returns `insufficient_data` until the scored response count reaches `min_sample_size` (default 30; override with `?minSampleSize=`). Read `rating` as a coarse bucket: `top_quartile` at p75+, `above_average` p50–75, `below_average` p25–50, `bottom_quartile` under p25.

## 6. Mine the verbatims (VoC)

`GET /api/v1/surveys/{id}/voc` groups the free-text `comment` verbatims your follow-up prompt collects: lexicon sentiment distribution, theme/topic extraction, per-theme driver analysis (score delta vs the overall average), an NPS/score trend over time, and a capped drill-down set of tagged verbatims.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/surveys/$SURVEY_ID/voc?granularity=week&channel=sms" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

Query knobs: `granularity` (`day`, `week`, `month` — the trend bucketing), and optional scorecard filters `channel`, `agent_id`, `team_id`, `segment_id` (agent/team attribute via the linked conversation's assignee; segment resolves membership dynamically). Scope them to build a per-agent or per-channel VoC scorecard.

**Response:**

```json theme={null}
{
  "survey_id": "$SURVEY_ID",
  "granularity": "week",
  "filters": { "channel": "sms", "agent_id": null, "team_id": null, "segment_id": null },
  "total_responses": 213,
  "total_comments": 178,
  "sentiment": {
    "total": 178,
    "avg_score": 0.31,
    "positive": 121,
    "neutral": 34,
    "negative": 23,
    "net_sentiment": 55.1
  },
  "themes": [
    { "theme": "delivery speed", "mentions": 41, "avg_sentiment": -0.62 },
    { "theme": "agent courtesy", "mentions": 38, "avg_sentiment": 0.81 }
  ],
  "drivers": [
    { "theme": "delivery speed", "mentions": 41, "avg_score": 5.9, "score_delta": -1.95 }
  ],
  "nps_trend": [
    { "bucket": "2026-07-27", "responses": 38, "nps_score": 29.0, "avg_score": 7.6 },
    { "bucket": "2026-08-03", "responses": 44, "nps_score": 34.6, "avg_score": 7.9 }
  ],
  "verbatims": [
    {
      "response_id": "rsp_…",
      "comment": "The package arrived a day late but the agent was great.",
      "score": 7,
      "sentiment": { "score": 0.33, "label": "positive" },
      "themes": ["delivery speed", "agent courtesy"]
    }
  ]
}
```

The drivers list is where you act: a theme mentioned often with a negative `score_delta` is dragging the average and belongs on the coaching or process backlog; a positive delta theme is what to amplify. Cap: the analytics consider at most 5,000 answered rows per call.

## 7. Wire it into a campaign or QA loop

Surveys attach cleanly to the post-campaign pattern: run a blast or drip, then distribute a CSAT/NPS follow-up to the same segment with `segment_id` on the send call, and chase with the remind loop. The full campaign sequence lives in [campaign end-to-end](/guides/campaign-end-to-end).

For service-quality programs, point `voc` at an agent or team filter and fold its drivers into [qa-workload-management](/guides/qa-workload-management) or the broader [quality-management-program](/guides/quality-management-program) scorecards — the benchmark percentile then tells you whether a coachable issue is also an outlier vs your vertical.

A low score also fires a `survey.response.detractor` webhook (for CRM or task follow-up) plus an in-app notification to your admins, so a detractor gets a same-day follow-up path rather than sitting in a month-end report. Subscribe to that event the way you would any webhook — see [webhook-consumer](/guides/webhook-consumer).
