Developer API Guide — File Import

Import audio, video, documents, and images into a Marvin project via API keys and presigned S3 uploads.

This is the standalone file import flow (one file → one Marvin interview/document). To attach media to survey respondents/responses as part of a CSV import, see Survey Import API Guide instead.

Prerequisites#

Create an API key with project:read + file: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 file: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 wav_key + presigned S3 upload URL
3. Upload to S3    →  POST the file directly to S3
4. Complete        →  signal Marvin to start processing
5. Poll status     →  wait for completed

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/files/initialize #

Creates a wav record and returns a wav_key plus a presigned S3 upload URL. Requires file:write scope. Rate limited to 20 requests/minute per API key.

bash
curl -s -X POST "$BASE_URL/api/v1/developer/import/files/initialize" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": 42,
    "file_name": "customer-interview.mp4"
  }' | python3 -m json.tool

Parameters:

FieldTypeRequiredDescription
project_idintegerYesID from the list projects response
file_namestringYesOriginal filename including extension (max 500 chars). Extension determines media type and content type.

Response (201):

json
{
  "wav_key": "a1b2c3d4-...",
  "upload_url": "https://s3.amazonaws.com/bucket/",
  "upload_fields": {
    "key": "developer-uploads/10/a1b2c3d4-.../media.mp4",
    "Content-Type": "video/mp4",
    "x-amz-credential": "...",
    "policy": "...",
    "x-amz-signature": "..."
  },
  "links": {
    "complete": "/api/v1/developer/import/files/a1b2c3d4-.../complete",
    "status": "/api/v1/developer/import/files/a1b2c3d4-.../status"
  }
}

Unsupported extensions return 400. See Supported File Types.


POST {upload_url} (S3) #

Upload directly to S3 via the presigned POST from initialize. All upload_fields must be sent as form fields before the file.

bash
curl -X POST "https://s3.amazonaws.com/bucket/" \
  -F "key=developer-uploads/10/a1b2c3d4-.../media.mp4" \
  -F "Content-Type=video/mp4" \
  -F "x-amz-credential=..." \
  -F "policy=..." \
  -F "x-amz-signature=..." \
  -F "file=@customer-interview.mp4"

Size limits (enforced by the presigned policy):

TypeMax size
Audio / video10 GB
Documents / images500 MB

Presigned URLs expire after 1 hour. To get a fresh URL, call initialize again (creates a new wav_key).


POST /api/v1/developer/import/files/{wav_key}/complete #

Signals Marvin that the S3 upload finished and starts processing (transcription for audio/video, extraction for documents, etc.).

bash
export WAV_KEY="a1b2c3d4-..."

curl -s -X POST "$BASE_URL/api/v1/developer/import/files/$WAV_KEY/complete" \
  -H "Authorization: Bearer $TOKEN" | python3 -m json.tool

Response (200):

json
{
  "wav_key": "a1b2c3d4-...",
  "status": "processing"
}

If the file is missing from S3:

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

Idempotency: Calling complete again when the file is already linked returns 200 with "status": "processing".


GET /api/v1/developer/import/files/{wav_key}/status #

Returns the current processing status for the uploaded file. Poll until status is completed or error.

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

Response:

json
{
  "wav_key": "a1b2c3d4-...",
  "name": "customer-interview.mp4",
  "status": "completed",
  "file_url": "https://s3.amazonaws.com/bucket/developer-uploads/10/a1b2c3d4-.../media.mp4",
  "created_at": "2026-07-14T08:30:00+00:00"
}

Status values:

StatusMeaning
pending_uploadInitialize succeeded; file not yet uploaded / not completed
processingFile linked; transcription or document processing in progress
completedReady to use in Marvin
errorProcessing failed (error_flag on the file)
recording(Rare for this API) Active recording session

Typical audio/video transcription latency depends on duration and queue load.


Supported File Types#

Extensions accepted by file_name (case-insensitive):

CategoryExtensions
Videomp4, mov, avi, webm, mkv, m4v, wmv, mpeg
Audiomp3, m4a, wav, flac, ogg, aac, aiff, aif, amr
Documentspdf, ppt, pptx, doc, docx, xls, xlsx, txt
Imagespng, jpg, jpeg, gif, bmp, tiff, webp

Error Reference#

HTTPWhen
400Invalid/missing body, unsupported extension, or file missing in S3 on complete
403Missing required scope
404Project not accessible / not found, or unknown wav_key
429Rate limit exceeded (20 req/min per API key on these endpoints)

End-to-End Example (Python)#

python
import json
import time
import requests

BASE_URL = "https://app.heymarvin.com"
CLIENT_ID = "cid-mrv_..."
CLIENT_SECRET = "sk-mrv_..."
PROJECT_ID = 42
LOCAL_FILE = "customer-interview.mp4"

# 1. Token
token = requests.post(
    f"{BASE_URL}/api/v1/oauth/token",
    data={
        "grant_type": "client_credentials",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "scope": "project:read file:write",
    },
).json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}

# 2. Initialize
init = requests.post(
    f"{BASE_URL}/api/v1/developer/import/files/initialize",
    headers=headers,
    json={"project_id": PROJECT_ID, "file_name": LOCAL_FILE},
).json()
wav_key = init["wav_key"]

# 3. Upload to S3
files = {"file": open(LOCAL_FILE, "rb")}
form = dict(init["upload_fields"])
requests.post(init["upload_url"], data=form, files=files).raise_for_status()

# 4. Complete
requests.post(
    f"{BASE_URL}/api/v1/developer/import/files/{wav_key}/complete",
    headers=headers,
).raise_for_status()

# 5. Poll until done
while True:
    status = requests.get(
        f"{BASE_URL}/api/v1/developer/import/files/{wav_key}/status",
        headers=headers,
    ).json()
    print(status["status"])
    if status["status"] in ("completed", "error"):
        break
    time.sleep(10)

print(json.dumps(status, indent=2))