Skip to main content
The /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 interpret respondents, 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.
Parameters marked planned for v1.4.0 are not yet available on api.honestly.com. They are documented here so you can plan your integration. Sending them today has no effect. Watch Versioning for the release.

Example Request

Example Response

When the response base is below the anonymity threshold, the score is suppressed:

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 no include_* parameters to set.
When the score is suppressed, 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: 0100 for favorability_percent, -100100 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 010 points, everything else normalized 0100. 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.
Planned change in v1.4.0. Today a suppressed score still returns the real cohort size, for example respondents: 3. From v1.4.0, respondents, responses and answers will all be null when suppressed is true, because a small cohort size is itself identifying. If your integration reads respondents on suppressed results, make it tolerate null before v1.4.0 ships.

Filtering by attribute

Planned for v1.4.0.
Pass attribute option IDs to narrow the score to a specific cohort. Options are discovered through the Attributes endpoints.
Options combine by attribute:
  • Options of the same attribute are OR-ed. 31,32 means Engineering or Sales.
  • Options of different attributes are AND-ed. 31,72 means Engineering in Berlin.
The order you pass IDs in never changes the result — they are grouped by attribute first. There is no limit on how many attributes you combine, but each additional attribute shrinks the cohort, so a three- or four-way filter is more likely to fall below the anonymity threshold and return a suppressed score. Only attributes released for API use by an account admin are accepted. Passing an option of a blocked attribute returns 400 rather than silently changing the cohort.

Score distribution

Planned for v1.4.0.
From v1.4.0 every response carries the distribution of scores behind the aggregate. There is nothing to opt into. The distribution is not banded. It lists one entry per scale point the requested questions can actually produce, with the number of answers that landed on exactly that point — so you get the shape of the original answer scale, not a histogram over invented ranges.

Where the values come from

For non-eNPS questions, every answer is stored as a normalized score on one common 0100 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, several question_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.
A union grid is uneven, and reading it as a bar chart will mislead — the 25 bar can only ever be fed by the five-point question, so it is short for a reason that has nothing to do with sentiment. When you intend to plot the distribution, request questions that share a scale, or aggregate the values yourself after export.

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 0100 would put a 010 question behind an axis nobody recognises, when the eleven-point scale is the thing eNPS readers already know.
All eleven points are always present, 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 -100100 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 0100 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.
Rules that apply to every scale:
  • Counts, not percentages. You can always divide; you cannot recover counts from a rounded percentage.
  • Counts sum to answers, not to respondents. 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 count over the whole array always gives answers.
  • null when the score is suppressed. The key is always present, but its value is null rather 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.
From v1.4.0 every response carries participation rates alongside the score, broken down by survey cycle. There is nothing to opt into.

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. So participation.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.
participation.cycles[].respondents is not the same number as the top-level respondents. The top-level count is people who answered the question_ids you requested. The per-cycle count is everyone who submitted a response in that cycle, whether or not they answered those particular questions. The per-cycle number is therefore the correct denominator companion for recipients, and is usually the larger of the two.

Cycles that only partly overlap your window

A cycle is included whole, with its full recipients 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.
Invitation-weighted is the definition to reach for unless you have a specific reason not to. If you settle on one, tell us at service@honestly.com and we can add it as an explicit field rather than leaving you to reimplement it.
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.
URL-encode datetime parameters in GET requests. The + sign in timezone offsets must be encoded as %2B. See Error Handling if you encounter 400 errors.