What API Keys Let You Do
API keys give external scripts, dashboards, and data pipelines access to your survey responses without requiring a browser login. Instead of exporting a file, your tool sends a request to Domandata and gets back structured JSON it can process immediately.
Common uses include pulling responses into a live research dashboard, syncing data to a statistical analysis environment, triggering downstream workflows when new responses arrive, and archiving data to a lab server on a schedule.
If you're using Claude with the Domandata MCP connected, you don't need to copy these examples by hand — ask Claude to write the script for you. It has a dedicated get_api_export_guide tool that gives it the exact endpoint paths, parameters, and response shapes from this page, so it won't guess at the API.
| Endpoint | Use it for | Rate limit |
|---|---|---|
GET /api/v1/export | Full dataset in one call — CSV, JSON, SPSS, or Stata; labeled or raw recode values | 10/min |
GET /api/v1/codebook | Question text, variable labels, options, and recode values — no respondent data | 60/min |
GET /api/v1/responses | Cursor-paginated raw JSON — for polling new responses incrementally | 60/min |
Export: Data Output
R: Skip the Boilerplate
If you're working in R, you don't need to write the httr/jsonlite calls shown later on this page yourself — source a small helper file we host instead:
source("https://www.domandata.net/r/domandata-helpers.R")That gives you four functions: dd_codebook() and dd_data() wrap the requests below into one call each, and dd_recode() / dd_label() convert a data frame between display labels and codebook recode values for multiple-choice and dropdown columns — the step covered in "Recode Labels to Values" further down — without you writing the label-to-value mapping by hand.
source("https://www.domandata.net/r/domandata-helpers.R")
# DOMANDATA_API_KEY must be set as an environment variable first
codebook <- dd_codebook(survey = "Customer Satisfaction Q3")
data <- dd_data(survey = "Customer Satisfaction Q3") # labels = TRUE by default
recoded <- dd_recode(data, codebook) # labels -> recode values
labeled <- dd_label(recoded, codebook) # recode values -> labels (inverse)It's a single sourceable file, not an installed package — nothing to run install.packages() on beyond httr, jsonlite, and readr, which the calls above already depend on. The rest of this page shows what it's doing under the hood, and still applies directly if you're in Python or another language.
Export: Data Output
Create an API Key
Step 1: Open Settings. From the top navigation, choose Settings.
Step 1: Open Settings
Step 2: Go to Advanced. Select the Advanced tab in Settings, then scroll to API Keys.
Step 2: Go to Advanced
Step 3: Start a new key. Click New API Key.
Step 3: Start a new key
Step 4: Name the key. Use the script or system name, such as R analysis pipeline or lab dashboard, so you can identify it later.
Step 4: Name the key
Step 5: Create and store it. Click Create, then copy the key immediately into your secrets manager or environment file.
Step 5: Create and store it
Step 6: Confirm ownership. Leave the Settings page only after you know which script owns the key and where the secret is stored.
Step 6: Confirm ownership
Each key has an internal prefix shown in the list so you can identify it later. Keys start with dmdt_.
Make a Request
Open the survey you want to pull from and copy the survey UUID from its URL. Pass the key as a Bearer token in the Authorization header. Replace YOUR_KEY with the key you copied and SURVEY_ID with the UUID.
GET /api/v1/responses?survey_id=SURVEY_ID
Authorization: Bearer YOUR_KEYExample with curl:
curl -H "Authorization: Bearer dmdt_your_key_here"
"https://www.domandata.net/api/v1/responses?survey_id=abc123"The response is JSON:
{
"data": [
{
"id": "uuid",
"survey_id": "uuid",
"created_at": "2026-05-23T12:00:00Z",
"answers": { "question-id": "value" },
"metadata": { "submission_source": "published" }
}
],
"next_cursor": "2026-05-23T11:00:00Z"
}Export: Data Output
Paginate Through Responses
Results are returned newest-first in pages of up to 50 by default (maximum 200). When there are more results, next_cursor contains a timestamp to pass as cursor on your next request.
# First page
GET /api/v1/responses?survey_id=SURVEY_ID&limit=100
# Next page - pass the cursor from the previous response
GET /api/v1/responses?survey_id=SURVEY_ID&limit=100&cursor=2026-05-23T11:00:00ZKeep requesting with the returned cursor until next_cursor is null, which means you have reached the end of the data.
Export: Data Output
Export Full Data as CSV
/api/v1/responses is built for polling — one page at a time. When you just want the whole dataset in one call (for example, to seed a local CSV in R and keep it updated), use /api/v1/export instead. It fetches every response server-side and returns it as a single CSV or JSON payload.
You can identify the survey by survey_id (its UUID) or by survey (its exact name, case-insensitive). Since survey names aren't required to be unique, an ambiguous name returns a 400 with a list of matching survey IDs to disambiguate with — pass survey_id going forward if that happens.
labels controls whether choice-type answers come back as their human-readable text (labels=true, the default) or their codebook recode values (labels=false) — the same distinction as the in-app CSV export's label/recode toggle.
curl -H "Authorization: Bearer dmdt_your_key_here"
"https://www.domandata.net/api/v1/export?survey=Customer%20Satisfaction%20Q3&labels=true"
-o responses.csvIn R:
library(httr)
library(readr)
key <- Sys.getenv("DOMANDATA_API_KEY")
resp <- GET(
"https://www.domandata.net/api/v1/export",
add_headers(Authorization = paste("Bearer", key)),
query = list(survey = "Customer Satisfaction Q3", format = "csv", labels = "true")
)
stop_for_status(resp)
data <- read_csv(content(resp, "text", encoding = "UTF-8"))In Python:
import os, requests, pandas as pd
from io import StringIO
key = os.environ["DOMANDATA_API_KEY"]
resp = requests.get(
"https://www.domandata.net/api/v1/export",
headers={"Authorization": f"Bearer {key}"},
params={"survey": "Customer Satisfaction Q3", "format": "csv", "labels": "true"},
)
resp.raise_for_status()
data = pd.read_csv(StringIO(resp.text))Pass format=json instead of the default format=csv to get the same rows back as JSON (each with a quality_assessment block), if that's more convenient for your pipeline than parsing CSV.
Full CSV/JSON exports are capped at 20,000 responses per call (SPSS/Stata has a separate, lower cap — see below) — a survey with more responses than that returns 413, and you should fall back to paginating through /api/v1/responses instead. Because each call re-fetches the whole dataset, keep polling for live updates on the cursor-based /api/v1/responses endpoint (see above) — reserve /api/v1/export for the initial pull and occasional full re-syncs.
Export: Data Output
Export as SPSS, Stata, or R
Pass format=sav, format=dta, or format=rdata to /api/v1/export instead of csv/json to get a native SPSS, Stata, or R data file — the same file the app's Deploy tab Export card produces, now available to any script or scheduled job that has an API key, not just the browser.
curl -H "Authorization: Bearer dmdt_your_key_here"
"https://www.domandata.net/api/v1/export?survey=Customer%20Satisfaction%20Q3&format=sav"
-o responses.savEvery column comes back numeric-coded with its value labels attached — single-select multiple-choice and dropdown options, radio-style grid rows, single-category card sort cards, MaxDiff Best/Worst picks, conjoint's Selected Alternative column, and the survey-language column when a survey has more than one — the format's own built-in equivalent of labels=true, so labels and flat don't apply to sav/dta/rdata and are ignored if passed. Variable names are sanitized and de-duplicated to fit both formats' naming rules (letters, digits, underscores, 32 characters); the variable label is the question's actual wording, not the short variable name, so haven::read_dta()/read_sav() in R gives every column both a real label (attr(col, "label")) and its value labels with no separate codebook join needed.
If you'd rather not add haven/labelled as a dependency at all, use format=rdata instead: it's a real .RData file, loadable with plain load() (no package needed), which binds a data frame named responses. Every categorical column is already a genuine base-R factor — not a haven_labelled vector needing as_factor() — with its levels in the survey's authored option order, and every column's variable label is a plain attr(col, "label"). A blank/unanswered cell is real NA in every column, including free-text ones (CSV/SPSS/Stata instead keep those as a literal empty string, since those formats have no equivalent "missing, not blank" marker).
These three formats build the whole file in memory in one request rather than streaming it, so they're capped separately and lower than CSV/JSON: 15,000 responses per call, not 20,000. A survey past that returns 413 — use format=csv or format=json instead (the labels=false CSV export carries the same numeric coding, just as text rather than a binary file).
Export: Data Output
Get the Codebook
/api/v1/codebook returns the survey's data dictionary — no respondent data, just question text, variable labels, answer options, and recode values — so you can label columns and build factors without opening the survey in the app. It accepts the same survey_id or survey parameter as the other endpoints.
curl -H "Authorization: Bearer dmdt_your_key_here"
"https://www.domandata.net/api/v1/codebook?survey_id=abc123"Each entry in questions gives you question_id (the key under answers in /api/v1/responses and /api/v1/export), variable_label (short column name), title (the full question text), and — for choice-bearing types — an options (or rows/columns/items) array of { raw_value, display_label, recode_value } triples:
{
"survey_id": "abc123",
"survey_name": "Customer Satisfaction Q3",
"questions": [
{
"question_id": "q1",
"variable_label": "satisfaction",
"title": "How satisfied are you with our service?",
"type": "multiple_choice",
"options": [
{ "raw_value": "Very satisfied", "display_label": "Very satisfied", "recode_value": "1" },
{ "raw_value": "Neutral", "display_label": "Neutral", "recode_value": "2" },
{ "raw_value": "Very dissatisfied", "display_label": "Very dissatisfied", "recode_value": "3" }
]
}
]
}In R, use it to build a lookup you can apply after pulling labels=false recode-value data from /api/v1/export:
library(httr)
library(jsonlite)
key <- Sys.getenv("DOMANDATA_API_KEY")
codebook <- fromJSON(content(GET(
"https://www.domandata.net/api/v1/codebook",
add_headers(Authorization = paste("Bearer", key)),
query = list(survey_id = "abc123")
), "text", encoding = "UTF-8"), flatten = TRUE)
# variable_label -> title, for renaming columns after import
var_labels <- setNames(codebook$questions$title, codebook$questions$variable_label)In Python:
import os, requests
key = os.environ["DOMANDATA_API_KEY"]
codebook = requests.get(
"https://www.domandata.net/api/v1/codebook",
headers={"Authorization": f"Bearer {key}"},
params={"survey_id": "abc123"},
).json()
# variable_label -> title, for renaming columns after import
var_labels = {q["variable_label"]: q["title"] for q in codebook["questions"]}Export: Data Output
Recode Labels to Values
"Recoding" means converting a choice-type answer between its human-readable text (display_label, e.g. "Very satisfied") and its codebook value (recode_value, e.g. "1"). There are two ways to get recoded data, depending on whether you already pulled it:
Starting fresh: pass labels=false to /api/v1/export (see "Export Full Data as CSV" above) and the API hands you already-recoded data directly — no client-side work.
Starting from data you already pulled with labels=true: build a display_label → recode_value map per question from /api/v1/codebook's options array, then apply it column by column. In R, dd_recode() from the helper file above does exactly this — and its inverse, dd_label(), converts the other direction:
source("https://www.domandata.net/r/domandata-helpers.R")
codebook <- dd_codebook(survey = "Customer Satisfaction Q3")
data <- dd_data(survey = "Customer Satisfaction Q3") # labels = TRUE
recoded <- dd_recode(data, codebook)It only touches multiple_choice and dropdown columns — those are the types whose CSV cell is a plain "label" or "label | label" string matching the codebook's options array 1:1. Other choice-bearing types (grid_matrix, constant_sum, drag_to_order, ...) store compound shapes (e.g. "Row: Column" pairs) that aren't a simple label swap, so those columns are left untouched rather than guessed at — for those, re-pull with labels=false instead.
In Python or another language, the same approach works without the helper: for each question in the codebook, build a dict from display_label to recode_value using its options, then .map()/replace that question's column.
Export: Data Output
API Best Practices
- Never commit keys to source control. Store keys in environment variables or a secrets manager, not in code files. If a key appears in a git commit, delete it immediately and create a new one.
- One key per system. Create a separate key for each script or integration. This makes it easy to revoke access for one system without disrupting others.
- Name keys descriptively. A name like Python export script or lab Shiny dashboard makes it clear what to revoke if a key is compromised.
- Use cursor-based pagination for large datasets. Fetching all responses in one large request is slower and more fragile. Page through results with
cursorso a network failure only loses one page, not the full export. - Handle errors gracefully. A 401 means the key is invalid or expired. A 403 means the key does not have access to that survey. A 429 means you are sending requests too fast - add a delay between pages. Log error codes rather than silently dropping data.
- Poll at a reasonable interval. If you are monitoring for new responses in real time, polling every 30-60 seconds is a reasonable cadence. Polling faster than once every few seconds provides little benefit and adds unnecessary load.
- Store the cursor between runs. If your script runs on a schedule, save the last
next_cursorvalue to disk or a database between runs. On the next run, start from that cursor to fetch only new responses rather than re-downloading everything. - Rotate keys periodically. For sensitive or long-running projects, create a new key, update your systems to use it, and delete the old key. Routine rotation limits exposure if a key was inadvertently logged or shared.
Export: Data Output
Delete a Key
Go to Settings, open Advanced, scroll to API Keys, and click the delete icon next to the key. Confirm when prompted. Any script using that key will immediately receive 401 errors. Create and distribute a new key before deleting an old one if the system needs continued access.
Export: Data Output