Import surveys
Import responses from a survey instrument — Qualtrics, SurveyMonkey, Typeform, or your own form. Marvin turns each row into a response and each mapped column into a question, so scored and categorical answers stay chartable instead of collapsing into text.
This is the questionnaire case. If your CSV is an operational export rather than a survey, use Import CSV. If it's mostly free text, use Import unstructured data. If participants recorded audio or video, use Import multimedia CSV.
Before you start#
You need an access token with the project:read and survey:write scopes. Add project:write too if you want to create the project via the API.
export BASE_URL="https://app.heymarvin.com"
export CLIENT_ID="cid-mrv_..."
export CLIENT_SECRET="sk-mrv_..."
TOKEN=$(curl -s -X POST "$BASE_URL/api/v1/oauth/token" \
-d "grant_type=client_credentials" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "scope=project:read survey:write" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
The token lasts 1 hour. See Access and authorization for the full setup.
The data#
A survey export has one column per question, and the column header is usually the question text:
Respondent Name,Email Address,Submitted At,Plan,How likely are you to recommend us?,Which features do you use?,What would you improve?
Alice Chen,alice@example.com,2026-03-02,Enterprise,9,"Transcripts;Search;Tags","Bulk export would save me a morning every week."
Bob Ortiz,bob@example.com,2026-03-03,Starter,4,"Search","Onboarding assumed I already knew what a codebook was."
Marvin treats the header as the question and each cell as that respondent's answer.
Matching columns to question types#
Map every question column to the type that reflects how it was asked — that's what decides whether an answer becomes a filterable chart or a piece of qualitative text.
{
"auto_map": false,
"column_mapping": {
"Respondent Name": "Name",
"Email Address": "Email",
"Submitted At": "Timestamp",
"Plan": "Single choice",
"How likely are you to recommend us?": "NPS",
"Which features do you use?": "Multi choice",
"What would you improve?": "Open-ended"
}
}
| Your question was | Map it to | Why |
|---|---|---|
| A 0–10 recommendation score | NPS | Charts as a Net Promoter Score, not an arbitrary number |
| Pick one option | Single choice | Becomes a survey filter |
| Pick all that apply | Multi choice | Becomes a multi-select filter |
| Rank these options | Ranking | Preserves order rather than reading as text |
| A free-text box | Open-ended | Becomes a qualitative note you can code |
| Screener or internal field | Do nothing | Kept out of the question set entirely |
auto_map can't see question types. The classifier only detects Name, Email, Timestamp, and Open-ended. Leave a scored or categorical question to it and the answer lands as text or is ignored — which is exactly the data you most wanted to chart. Map question columns explicitly.
Full type list and resolution order: Column types. Other mapping strategies: choosing a mapping strategy.
Respondents and the Research Panel#
add_to_research_panel defaults to true, which adds each respondent to your Research Panel using the Name and Email columns. That's usually what you want for a survey — it's how you recontact people for follow-up research.
Set it to false for anonymous surveys, or when the responses came from a panel provider you don't own the relationship with.
{ "add_to_research_panel": false }
Run the import#
1. List projects → pick a project_id
2. Initialize → get presigned S3 upload URLs
3. Upload to S3 → POST CSV parts directly to S3
4. Complete → signal Marvin to start processing
5. Poll status → wait for COMPLETED
Step details are in Import CSV; endpoint specifics are in the CSVs API reference.
Surveys that are still in field are the common case for adding rows later — append new responses as they come in rather than re-importing. Append reuses the original mapping, and every appended part must carry a header row with all of the existing survey's question columns.
Complete example#
import time
import requests
BASE_URL = "https://app.heymarvin.com/api"
# 1. Get access token
access_token = requests.post(
f"{BASE_URL}/v1/oauth/token",
data={
"grant_type": "client_credentials",
"client_id": "cid-mrv_your_client_id_here",
"client_secret": "sk-mrv_your_secret_here",
"scope": "project:read survey:write",
},
).json()["access_token"]
headers = {"Authorization": f"Bearer {access_token}"}
# 2. Pick a project
projects = requests.get(
f"{BASE_URL}/v1/developer/import/projects",
headers=headers,
).json()["projects"]
project_id = projects[0]["id"]
# 3. Initialize — one entry per question column
init = requests.post(
f"{BASE_URL}/v1/developer/import/surveys/initialize",
headers=headers,
json={
"project_id": project_id,
"name": "Q1 Product Satisfaction Survey",
"file_type": "CSV",
"num_parts": 1,
"add_to_research_panel": True,
"column_mapping": {
"Respondent Name": "Name",
"Email Address": "Email",
"Submitted At": "Timestamp",
"Plan": "Single choice",
"How likely are you to recommend us?": "NPS",
"Which features do you use?": "Multi choice",
"What would you improve?": "Open-ended",
},
},
).json()
upload_key = init["upload_key"]
part = init["upload_urls"][0]
# 4. Upload the responses to S3
with open("responses.csv", "rb") as f:
requests.post(
part["upload_url"],
data=part["upload_fields"],
files={"file": f},
).raise_for_status()
# 5. Complete
requests.post(
f"{BASE_URL}/v1/developer/import/surveys/{upload_key}/complete",
headers=headers,
).raise_for_status()
# 6. Poll until done
while True:
status = requests.get(
f"{BASE_URL}/v1/developer/import/surveys/{upload_key}/status",
headers=headers,
).json()
if status["status"] == "COMPLETED":
summary = status["processing"]
print(f"{summary['total_responses']} responses, {summary['questions_detected']} questions.")
break
if status["status"] == "FAILED":
print(f"Import failed: {status['error_details']}")
break
time.sleep(5)
Next steps#
- Import multimedia CSV — when responses come with audio or video
- CSVs API reference — exact parameters, responses, and status values
- Troubleshooting — partial failures and append rejections