/v1/scores endpoint returns a single aggregated, anonymity-protected engagement score for a specified set of questions over a given time window. Use it to track engagement trends in your warehouse without exposing individual responses.
The score is aggregated over every response to the requested question_ids within the period_from / period_to window. When the response base is below the anonymity threshold, the score is suppressed (score: null, suppressed: true) so you can distinguish “hidden for privacy” from “no data”. The threshold is configured per survey; when the requested questions span multiple surveys, the strictest threshold applies.
All requested questions must be of the same kind — either all eNPS questions or all non-eNPS questions. Mixing eNPS and non-eNPS questions in one request returns a 400 error. The unit field tells you which kind was aggregated.
What the score is calculated over
The score is averaged over individual question answers, not over people. One person contributes one answer per question they answered, in each survey cycle they responded to. If you request two questions and a person answered both in two separate cycles within your time window, they contribute four answers to the average. This matters when you interpretrespondents, which counts something different — see Response fields below.
Endpoint
Query Parameters
string
required
Comma-separated question IDs to aggregate into a single score, for example
121,122. These are the same integer IDs returned by the /v1/questions endpoint.string
required
RFC 3339 datetime. Only aggregate responses given at or after this date.
string
required
RFC 3339 datetime. Only aggregate responses given at or before this date.
string
Planned for v1.4.0. Comma-separated survey IDs. Restricts the aggregation to responses from these surveys — useful when the same question appears in more than one survey and you want them kept apart. Omit to aggregate across every survey the questions belong to.
string
Planned for v1.4.0. Comma-separated attribute option IDs from
/v1/attributes/{attribute_id}/options. Restricts the score to matching respondents. See Filtering by attribute.To build a heatmap, call this endpoint once per cell — pass the same
question_ids each time and vary attribute_option_ids over the options you want as rows. Each call is anonymity-checked on its own cohort, so a cell covering too few people comes back suppressed while the rest of the grid still resolves.Example Request
Example Response
Planned v1.4.0 response
From v1.4.0 the same request returns the counts, the score distribution and the per-cycle participation block as well. None of them is opt-in — there are noinclude_* parameters to set.
distribution is null while participation is still returned — participation is governed by its own per-cycle threshold, not by the score’s:
Response Fields
number | null
The aggregated score. Its range depends on
unit: 0–100 for favorability_percent, -100–100 for enps. null when suppressed is true.string
The unit of the
score value. favorability_percent (0–100) for non-eNPS questions — each question is normalized to 0–100 and the results are averaged; enps (-100–100) when all requested questions are eNPS questions.integer
Number of distinct people who answered. Somebody who responded in more than one survey cycle within your time window counts once.This is not the number of values the score was averaged over — see What the score is calculated over. Use it to judge how many people are behind a score, not as the sample size for a statistical calculation.The count is also returned when
suppressed is true. This changes in v1.4.0 — see the warning below.integer | null
Planned for v1.4.0. Number of distinct submissions. The same person answering in two survey cycles counts twice. Equals
respondents only when your time window covers a single cycle.integer | null
Planned for v1.4.0. Number of individual question answers — the base the score is averaged over, and the total the score distribution sums to.
object | null
Planned for v1.4.0, always returned. How many answers landed on each point of the answer scale.
null when the score is suppressed, or when no requested question carries a score. Read scale before using the values — eNPS reports raw 0–10 points, everything else normalized 0–100. See Score distribution.boolean
Indicates the score was hidden because the response base is below the account anonymity threshold.
string | null
Reason the score was suppressed. Currently
below_anonymity_threshold. null when suppressed is false.Scores are anonymity-protected. The threshold is compared against
respondents — the number of distinct people — so repeated responses from the same person can never lift a score above the threshold. When the count is below the per-survey threshold, score is null, suppressed is true, and suppression_reason explains why, rather than returning an error.Filtering by attribute
Planned for v1.4.0.
- Options of the same attribute are OR-ed.
31,32means Engineering or Sales. - Options of different attributes are AND-ed.
31,72means Engineering in Berlin.
400 rather than silently changing the cohort.
Score distribution
Planned for v1.4.0.
Where the values come from
For non-eNPS questions, every answer is stored as a normalized score on one common0–100 scale, independent of how many points its question used. The value is derived from the answer option the respondent picked: the question’s worst option normalizes to 0, its best option to 100, and the options in between are spaced evenly across that range.
So the set of values a question contributes is fixed by how many options it has:
eNPS is the exception: its distribution reports the answer points as respondents gave them.
You can compute the normalized value yourself from
/v1/questions: each answer option carries a value, and the options flagged is_minimum and is_maximum are the two ends of the range. Use those flags rather than assuming the highest value is the best answer — on some question types the numbering runs the other way, and the flags are what tells you which end is which.
Every available value appears, even at zero
values lists every scale point the requested questions offer, whether or not anybody chose it. An option nobody picked comes back with count: 0 rather than being omitted, so the array length is stable across periods and cohorts, and a chart keeps its axis when a cell empties out.
Requesting questions with different scales
Because non-eNPS answers are normalized, severalquestion_ids aggregate into a single distribution — a five-point question and a seven-point question can be read together, which counting raw answer options cannot do.
When the requested questions do not share a scale, values is the union of the values they each produce, ascending. A five-point question (0, 25, 50, 75, 100) requested alongside an eleven-point one yields the eleven-point grid plus 25 and 75, and each entry counts only the answers that landed on that exact point.
eNPS
eNPS is the one case where the distribution is not normalized.scale is enps and values reports the eleven raw answer points 0 through 10, exactly as respondents gave them — rescaling them to 0–100 would put a 0–10 question behind an axis nobody recognises, when the eleven-point scale is the thing eNPS readers already know.
count: 0 included. The segments the eNPS score is built from are contiguous runs of them, so you can group them yourself:
The eNPS
score itself stays on its own -100–100 range — it is the promoter share minus the detractor share, not an average of these points. So within one eNPS response, score, distribution.values[].score and the 0–100 favorability scale are three different ranges. Branch on unit before you plot anything.
A request is either all eNPS or all non-eNPS — mixing the two returns
400 — so a single distribution never mixes the raw and normalized scales. scale therefore describes the whole array, and you only need to read it once per response.- Counts, not percentages. You can always divide; you cannot recover counts from a rounded percentage.
- Counts sum to
answers, not torespondents. Someone who answered the same question in two cycles contributes two counts and one respondent. - Values are exact, not ranges. Nothing falls between two entries, so summing
countover the whole array always givesanswers. nullwhen the score is suppressed. The key is always present, but its value isnullrather than a list — a distribution over a handful of people is more identifying than a single average.
Normalized scores exist for scale-type questions — star ratings, eNPS, smiley scales and ranked selects. Questions outside that set contribute to neither the score nor the distribution, so if you request only such questions
distribution is null. This is the same set of questions the score itself covers, so the two are never inconsistent.Participation rate
Planned for v1.4.0.
Why it is broken down by cycle
A participation rate is respondents divided by invitees. The number of respondents can be counted for any date range, but the number of invitees cannot — nobody is invited to a date range. Invitations exist per survey cycle. Soparticipation.cycles lists every cycle that overlaps your period_from / period_to window, each carrying its own denominator. That keeps the rate well-defined however wide a window you request.
object
Always present from v1.4.0.
Cycles that only partly overlap your window
A cycle is included whole, with its fullrecipients and respondents, whenever any part of it falls inside your window. Its own opened_at and closed_at tell you it extends beyond what you asked for, so you can keep it or drop it.
Cycles are deliberately not clipped to your window. Clipping would count only the responses inside the range against the full list of invitees, which understates participation without any signal that it happened.
Aggregating across cycles
If you need one participation number for the whole window, aggregate the rows yourself — but choose the definition deliberately, because the two obvious ones disagree.Participation exposes headcount, not survey content, so
recipients is always returned even for small cycles. respondents and rate follow the same anonymity threshold as scores.