Importing insights into Marvin
Create an Insight in Marvin by uploading a single document — PDF, PPT/PPTX, DOC/DOCX, XLS/XLSX, or TXT. This mirrors what happens when a user drags a file into the Insights tab: Marvin converts the document (via Google Drive) into a viewable PDF, extracts a thumbnail, and — once you signal completion — runs the same background processing the web app uses.
Insights are one document each, not data. For tabular or survey data, use Import CSV. For audio, video, or files you want transcribed rather than published, use Importing files to Marvin.
Before you start#
You need an access token with the project:read and insight: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 insight: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.
How the flow works#
1. List projects → pick a project_id
2. Initialize → get a presigned S3 upload URL
3. Upload to S3 → POST the document directly to S3
4. Complete → signal Marvin to start processing
5. Poll status → wait for "completed"
The document goes straight to S3, not through Marvin — which is why the flow has an initialize step (to get a signed URL) and a complete step (to tell Marvin the bytes have landed). There's no chunking: one document is one upload.
1. Pick a project#
GET /import/projects returns the projects your key can reach. Take an id from the response, or create a project if you need a new one.
2. Initialize the upload#
POST /import/insights/initialize with the project_id and the filename. The extension in filename determines the document type, so send the real name.
You get back an insight_key plus upload_url and upload_fields for S3. Keep the insight_key — every later call is keyed on it.
Initialize has no idempotency key. If you retry it, you get a second insight. Cache the insight_key before uploading so a retry elsewhere in your code doesn't duplicate the document.
3. Upload the document to S3#
POST to upload_url with every entry from upload_fields as a form field, ordered before the file field. Documents are capped at 500 MB and the signed URL expires after an hour.
4. Complete#
POST /import/insights/{insight_key}/complete starts processing. This is also where you decide visibility: pass should_publish: true to publish once conversion finishes, or leave it off to keep the insight as a private draft you publish later in the UI.
You don't resend the filename here — Marvin derives the document type and S3 key from what you gave at initialize, so the two can't disagree.
5. Poll for status#
GET /import/insights/{insight_key}/status until processing_status is completed or failed. A successful response also carries the generated thumbnail_image and the resolved insight_type.
Poll every 5 seconds or so — status calls count against the 20 requests/minute limit.
To list insights you've imported, use GET /import/insights — paginated, newest first.
Complete example#
import time
import requests
BASE_URL = "https://app.heymarvin.com/api"
# 1. Get access token
token_resp = 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 insight:write",
},
)
access_token = token_resp.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 the insight upload
filename = "q1-research-summary.pdf"
init_resp = requests.post(
f"{BASE_URL}/v1/developer/import/insights/initialize",
headers=headers,
json={"project_id": project_id, "filename": filename},
)
init_data = init_resp.json()
insight_key = init_data["insight_key"]
# 4. Upload the document to S3 (max 500 MB)
with open(filename, "rb") as f:
requests.post(
init_data["upload_url"],
data=init_data["upload_fields"],
files={"file": f},
).raise_for_status()
# 5. Complete the upload
requests.post(
f"{BASE_URL}/v1/developer/import/insights/{insight_key}/complete",
headers=headers,
json={"should_publish": False},
).raise_for_status()
# 6. Poll until done
while True:
status = requests.get(
f"{BASE_URL}/v1/developer/import/insights/{insight_key}/status",
headers=headers,
).json()
state = status["processing_status"]
print(f"Status: {state}")
if state == "completed":
print(f"Insight ready: {status['insight_key']}")
break
elif state == "failed":
print("Insight processing failed.")
break
time.sleep(5)
Limits at a glance#
| Accepted extensions | pdf, ppt, pptx, doc, docx, xls, xlsx, txt |
| Max document size | 500 MB |
| Presigned URL lifetime | 1 hour |
| Rate limit | 20 requests/minute per API key |
| Default visibility | Private draft (insight_state: 1) |
Next steps#
- Insights API reference — exact parameters, responses, and status values
- Troubleshooting — S3 upload failures, duplicate insights, drafts that didn't publish