Skip to main content

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.

1. Author the survey template

Create the template with POST /api/v1/surveys. The key fields:
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).
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:
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.
Response:
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.
Response:
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.
Response:
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.
Response:
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.
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:
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. For service-quality programs, point voc at an agent or team filter and fold its drivers into qa-workload-management or the broader 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.