Developer API Guide — Insight Import

Create an Insight in Marvin by uploading a document — PDF, PPT/PPTX, DOC/DOCX, XLS/XLSX, or TXT — programmatically via API keys and presigned S3 uploads. 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 — kicks off the same background processing the web app uses.

This is a separate resource type from surveys. Insights are not CSV data; each upload is a single document. See the Survey Import API Guide for importing survey response data.

Prerequisites#

Create an API key with project:read + insight:write scopes and exchange it for a JWT. See API Key Integration Guide for the full auth setup.

bash
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 expires in 1 hour. Exchange again when it expires.

Optional: include project:write if you need to create a project via the API.


Import Flow#

text
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"

There's no chunking (one document = one upload) and no idempotency key — retrying initialize creates a new Insight each time.


GET /api/v1/developer/import/projects #

Returns projects accessible to your API key. Requires project:read scope. Archived and deleted projects are excluded.

bash
curl -s "$BASE_URL/api/v1/developer/import/projects" \
  -H "Authorization: Bearer $TOKEN" | python3 -m json.tool

Response:

json
{
  "projects": [
    { "id": 42, "name": "Alpha Research" },
    { "id": 43, "name": "Beta Onboarding" }
  ]
}

Team-shared keys see team projects; personal keys see user-accessible projects.


POST /api/v1/developer/import/insights/initialize #

Creates an insight upload and returns a presigned S3 URL. Requires insight:write scope.

bash
curl -s -X POST "$BASE_URL/api/v1/developer/import/insights/initialize" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": 42,
    "filename": "q1-research-summary.pdf"
  }' | python3 -m json.tool

Parameters:

FieldTypeRequiredDescription
project_idintegerYesID from the list projects response
filenamestringYesOriginal filename, used to determine the document type. Extension must be one of pdf, ppt, pptx, doc, docx, xls, xlsx, txt

should_publish is specified on complete, not here.

Response (201):

json
{
  "insight_key": "e5f6a7b8-...",
  "upload_url": "https://s3.amazonaws.com/bucket/",
  "upload_fields": {
    "key": "developer-uploads/10/e5f6a7b8-.../doc.pdf",
    "Content-Type": "application/pdf",
    "x-amz-credential": "...",
    "policy": "...",
    "x-amz-signature": "..."
  },
  "links": {
    "complete": "/api/v1/developer/import/insights/e5f6a7b8-.../complete",
    "status": "/api/v1/developer/import/insights/e5f6a7b8-.../status"
  }
}

Rate limited to 20 requests/minute per API key.


POST {upload_url} (S3) #

Upload the document directly to S3 via the presigned POST from initialize. All upload_fields must be sent as form fields before the file. The document is capped at 500 MB.

bash
curl -X POST "https://s3.amazonaws.com/bucket/" \
  -F "key=developer-uploads/10/e5f6a7b8-.../doc.pdf" \
  -F "Content-Type=application/pdf" \
  -F "x-amz-credential=..." \
  -F "policy=..." \
  -F "x-amz-signature=..." \
  -F "file=@q1-research-summary.pdf"

Presigned URLs expire after 1 hour.


POST /api/v1/developer/import/insights/{insight_key}/complete #

Signals Marvin that the S3 upload finished and starts document processing.

bash
curl -s -X POST "$BASE_URL/api/v1/developer/import/insights/$INSIGHT_KEY/complete" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "should_publish": false
  }' | python3 -m json.tool

Parameters:

FieldTypeRequiredDescription
should_publishbooleanNoPublish the insight immediately once processing finishes (default: false, insight stays a private draft)

The document type and S3 key are derived from the filename you gave at initialize time — you don't resend it here. This avoids a mismatch if a different filename were passed to complete than was used to compute the upload's S3 key.

Response (202):

json
{
  "insight_key": "e5f6a7b8-...",
  "status": "processing"
}

If the document hasn't finished uploading to S3 yet, this returns 400:

json
{
  "error": "File not found in S3. Upload the file before calling complete."
}

Idempotency: Calling this endpoint again once the insight has reached completed or failed returns 200 with the current status instead of re-triggering processing. A duplicate call that races the original request while it's still processing (processing_status stays pending for the whole duration) is also caught — the row is locked for the duration of the request, so the duplicate reliably sees the in-progress document and returns 200 with "message": "Upload is already processing; see status endpoint." instead of enqueueing a second job.


GET /api/v1/developer/import/insights/{insight_key}/status #

Returns the current processing status for the insight upload. Poll until processing_status is completed or failed.

bash
curl -s "$BASE_URL/api/v1/developer/import/insights/$INSIGHT_KEY/status" \
  -H "Authorization: Bearer $TOKEN" | python3 -m json.tool

Response:

json
{
  "insight_key": "e5f6a7b8-...",
  "name": "q1-research-summary.pdf",
  "processing_status": "completed",
  "insight_state": 1,
  "insight_type": "pdf",
  "thumbnail_image": "https://s3.amazonaws.com/bucket/insights/thumbnail/....png",
  "created_at": "2026-07-10T00:00:00Z"
}

processing_status values:

StatusMeaning
pendingDocument not yet processed — awaiting complete or processing is in progress
completedConversion finished — insight available in Marvin
failedProcessing failed

insight_state values:

ValueMeaning
0Published — visible to the team
1Private draft — visible only to the creator
2Shared draft

An insight starts as a private draft (1) at initialize time; once processing completes it becomes 0 (published) if should_publish was true, or stays 1 otherwise.

insight_type values: pdf, ppt, sheet, doc, marvin_doc, other — derived from the uploaded file's extension.


Complete Python Example#

python
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)