How to join structure and rounds
Structure and rounds are the two Attest API endpoints. Learn how they connect, what the key concepts mean, and how to join them to resolve respondent answers to question titles and labels.
When you call the Attest API, results are split across two endpoints: structure and rounds. To work with the data meaningfully, you need both.
- Structure returns the schema of a study: the questions, answer options, wave metadata, and audience information. Use it to understand what was asked and how the survey was organised.
- Rounds returns the response data: one record per respondent, with the answers they gave, demographic attributes, and outcome information. Use it to analyse results, segment respondents, and track how answers change across waves.
Neither endpoint is fully useful on its own. Structure tells you what the questions and answers mean. Rounds tells you what people said. You join them together using IDs.
How they connect
Everything in the Attest API is identified by a UUID. The same IDs appear in both endpoints, which is how you link a respondent's answer back to the question it belongs to.
The join works like this:
- Each answer option in
structurehas anid - Each respondent record in
roundscontains acardsobject, where each card holds the answers the respondent gave - Every answer object carries a
fieldIdthat matches an answer optionidinstructure— usefieldIdto resolve the answer, rather than the answer's own dict key (see "Fields and answer options" below for why)
Key concepts
Nodes
A node is a question. The nodes[] array in the structure response contains one object per question, with its title, type, and answer options.
Fields and answer options
Each node has a fields.items[] array containing its answer options. Each option has an id and a text label.
Every answer object in a round's cards also carries a fieldId, set to the matching option's id in structure. Use fieldId to do the join, not the answer's dict key. For most question types the dict key and fieldId are identical, so it doesn't matter which one you use — but for grid questions (see below) they're different, and fieldId is the one that actually resolves. Using fieldId consistently means the same join logic works across every question type without a special case for grids.
Cards
A card represents one respondent's answers to one question. Cards are returned inside each round object, in a cards object keyed by card ID. Card IDs match node IDs directly, so you can join them to the corresponding question in structure without needing to match through answer IDs. See GET Study Structure and GET Study Rounds for the full field reference.
Skipped questions
If a respondent skips a question, the card will contain a single answer keyed "skipped" with no corresponding entry in structure. This is expected — skipped is a system value, not an answer option. When building a join, filter out any answer with a fieldId of "skipped" before looking up in structure.
Waves
A study can run across multiple waves: repeated rounds of the same survey sent to an audience at different points in time. Each wave has a waveIndex and a publishedAt timestamp. Wave indexes are stable across API calls. Use positive indexes to count from the oldest wave (0 is the first), or negative indexes to count from the newest (-1 is the most recent).
Subjects (grid questions only)
Grid questions have both fields.items[] (the scale, e.g. 0–10) and subjects.items[] (the rows, e.g. "brush your teeth", "floss"). A grid answer's dict key is a composite ID that doesn't match anything in structure directly — don't try to decode it. Instead, every grid answer carries both a fieldId (which scale point was picked) and a subjectId (which row it was picked for), each matching an id in the node's fields.items[] and subjects.items[] respectively. Join on those two fields the same way you'd join fieldId for any other question type.
Question types
The type field on a node determines how to interpret the answers in rounds data.
| Type | Description | Answer shape |
|---|---|---|
single_choice | One answer selected | One answer ID |
multiple_choice | One or more answers selected | Multiple answer IDs |
nps | 0–10 scale | One answer ID corresponding to the selected value |
ranked | Options ranked in order | Multiple answer IDs, each with an order field indicating rank position |
free_text | Open text response | Answer contains a text field and optionally a sentiment field |
grid | Scale applied across multiple row items | Composite answer IDs that encode both the subject and scale value |
max_diff | Best/worst scaling | Answer contains a maxDiff object keyed by answer ID, each with a label field indicating best or worst |
Example: resolving an answer
Given this answer option in structure:
{
"id": "{fieldId}",
"text": "Daily"
}And this card in a round:
{
"answers": {
"{answerId}": {
"fieldId": "{fieldId}",
"order": 1,
"text": "Daily"
}
}
}Resolve the answer using fieldId, not the outer {answerId} key — fieldId is the one guaranteed to match structure's answer option id. Here it tells you this respondent selected Daily as their answer.
Example: resolving a grid answer
Given this field and subject in structure:
{
"fields": { "items": [{ "id": "{fieldId}", "text": "Strongly agree" }] },
"subjects": { "items": [{ "id": "{subjectId}", "text": "Benefit" }] }
}And this answer in a round:
{
"answers": {
"{compositeAnswerId}": {
"fieldId": "{fieldId}",
"subjectId": "{subjectId}",
"order": 4
}
}
}The outer key ({compositeAnswerId}) is opaque and not meant to be decoded. Use fieldId and subjectId instead: together they tell you this respondent rated "Benefit" (the subject) as "Strongly agree" (the field) for this row of the grid.
Updated about 1 month ago