Connecting to BI Tools

Attest data can be connected to BI tools like Power BI and Tableau. Because the rounds endpoint returns nested JSON, you'll need to prepare the data into a flat table first, then connect your BI tool to that.

Prepare the data

When a BI tool flattens the rounds JSON, answer IDs become column names rather than row values, which makes the data impossible to join or chart meaningfully. To avoid this, transform the rounds data into a flat table that resolves answer IDs to question titles and answer labels, and joins rounds to structure in a single step.

There are a few ways to produce this flat table, depending on your setup.

Data engineers

If your team has data engineers, the rounds response can be transformed using your existing tooling. See guide on combining structure and rounds to analyse results for how the two endpoints relate.

Python script

For most teams, the simplest option is to run the script below. It calls both endpoints, joins them, and outputs a single flat CSV. Set your API key and study ID at the top, then run it.

"""
Attest API — fetch and flatten study data into a tidy DataFrame.

Requirements: pip install requests pandas
Set your API key: export ATTEST_API_KEY=your_key
"""

import os
import requests
import pandas as pd

API_KEY  = os.environ["ATTEST_API_KEY"]
STUDY_ID = "your_study_id"  # replace with your Study ID
BASE_URL = "https://api.askattest.com/insights/api/v1"
HEADERS  = {"X-API-Key": API_KEY}


# ── FETCH ─────────────────────────────────────────────────────────────────────

def fetch_structure(study_id):
    r = requests.get(f"{BASE_URL}/studies/{study_id}/structure", headers=HEADERS)
    r.raise_for_status()
    return r.json()


def fetch_rounds(study_id, survey_id):
    r = requests.get(
        f"{BASE_URL}/study/{study_id}/rounds",
        headers=HEADERS,
        params={"surveyIds": survey_id},
    )
    r.raise_for_status()
    return r.json()


# ── BUILD ANSWER LOOKUP ───────────────────────────────────────────────────────

def build_answer_lookup(structure):
    """Map answer_id -> question and answer metadata from the structure response."""
    lookup = {}

    for node in structure["nodes"]:
        q_title = node["title"]
        q_type  = node["type"]

        if q_type == "grid":
            subjects = node.get("subjects", {}).get("items", [])
            for field in node["fields"]["items"]:
                for ans_id in field.get("answerIds", []):
                    for subject in subjects:
                        if ans_id in subject.get("answerIds", []):
                            lookup[ans_id] = {
                                "question_title": q_title,
                                "question_type":  q_type,
                                "answer_label":   field["text"],
                                "subject":        subject["text"],
                            }
        else:
            for field in node["fields"]["items"]:
                ans_ids = field.get("answerIds", [field.get("id")])
                for ans_id in ans_ids:
                    if ans_id:
                        lookup[ans_id] = {
                            "question_title": q_title,
                            "question_type":  q_type,
                            "answer_label":   field["text"],
                            "subject":        None,
                        }

    return lookup


# ── FLATTEN ───────────────────────────────────────────────────────────────────

def flatten(rounds, answer_lookup):
    """
    One row per answer per respondent, with demographics.

    Demographics are read dynamically from each round — no hardcoded field names.
    'skipped' answer IDs are filtered out (system value, not a real answer option).
    """
    rows = []

    for round_ in rounds:
        round_id  = round_["id"]
        survey_id = round_.get("survey", {}).get("id")
        outcome   = round_.get("outcome")
        wave_ts   = round_.get("waveTimestamp")
        demo      = round_.get("demographics", {})

        for card in round_.get("cards", {}).values():
            for ans_id, ans_data in card.get("answers", {}).items():
                if ans_id == "skipped":
                    continue

                info = answer_lookup.get(ans_id)
                if not info:
                    continue

                row = {
                    "round_id":       round_id,
                    "survey_id":      survey_id,
                    "wave_timestamp": wave_ts,
                    "outcome":        outcome,
                    "question_title": info["question_title"],
                    "question_type":  info["question_type"],
                    "subject":        info["subject"],
                    "answer_label":   info["answer_label"],
                    "answer_order":   ans_data.get("order"),
                    "answer_text":    ans_data.get("text"),
                    "sentiment":      ans_data.get("sentiment"),
                }

                # Dynamically include all demographic fields
                for key, value in demo.items():
                    row[f"demo_{key}"] = value

                rows.append(row)

    return pd.DataFrame(rows)


# ── RUN ───────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    structure = fetch_structure(STUDY_ID)
    print(f"Fetched structure: {len(structure['nodes'])} nodes, {len(structure['surveys'])} surveys")

    # Use all surveys, or replace with a specific survey ID
    survey_ids = [s["id"] for s in structure["surveys"]]

    all_rounds = []
    for survey_id in survey_ids:
        rounds = fetch_rounds(STUDY_ID, survey_id)
        all_rounds.extend(rounds)
        print(f"  Survey {survey_id}: {len(rounds)} rounds")

    answer_lookup = build_answer_lookup(structure)
    df = flatten(all_rounds, answer_lookup)

    print(f"\nFlattened DataFrame: {len(df)} rows, {len(df.columns)} columns")
    print(df.head())

    df.to_csv("attest_results.csv", index=False)
    print("\nSaved to attest_results.csv")

The result is a flat table where each row is one answer from one respondent, with question titles, answer labels, and demographics as columns.

Connect data to Power BI

  1. In Power BI Desktop, click Get Data
  2. Connect to your CSV file or data warehouse
  3. The data is ready to chart with no joining required

Connect data to Tableau

  1. In Tableau, add a new data source
  2. Connect to your CSV file or data warehouse
  3. The data is ready to chart with no joining required