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:

  1. Each answer option in structure has an id
  2. Each respondent record in rounds contains a cards object, where each card holds the answers the respondent gave
  3. Every answer object carries a fieldId that matches an answer option id in structure — use fieldId to 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.

TypeDescriptionAnswer shape
single_choiceOne answer selectedOne answer ID
multiple_choiceOne or more answers selectedMultiple answer IDs
nps0–10 scaleOne answer ID corresponding to the selected value
rankedOptions ranked in orderMultiple answer IDs, each with an order field indicating rank position
free_textOpen text responseAnswer contains a text field and optionally a sentiment field
gridScale applied across multiple row itemsComposite answer IDs that encode both the subject and scale value
max_diffBest/worst scalingAnswer 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.


Did this page help you?