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.
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#
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.
curl -s "$BASE_URL/api/v1/developer/import/projects" \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
Response:
{
"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.
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:
| Field | Type | Required | Description |
|---|---|---|---|
project_id | integer | Yes | ID from the list projects response |
file_name | string | Yes | Original filename including extension (max 500 chars). Extension determines media type and content type. |
Response (201):
{
"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.
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):
| Type | Max size |
|---|---|
| Audio / video | 10 GB |
| Documents / images | 500 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.).
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):
{
"wav_key": "a1b2c3d4-...",
"status": "processing"
}
If the file is missing from S3:
{
"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.
curl -s "$BASE_URL/api/v1/developer/import/files/$WAV_KEY/status" \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
Response:
{
"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:
| Status | Meaning |
|---|---|
pending_upload | Initialize succeeded; file not yet uploaded / not completed |
processing | File linked; transcription or document processing in progress |
completed | Ready to use in Marvin |
error | Processing 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):
| Category | Extensions |
|---|---|
| Video | mp4, mov, avi, webm, mkv, m4v, wmv, mpeg |
| Audio | mp3, m4a, wav, flac, ogg, aac, aiff, aif, amr |
| Documents | pdf, ppt, pptx, doc, docx, xls, xlsx, txt |
| Images | png, jpg, jpeg, gif, bmp, tiff, webp |
Error Reference#
| HTTP | When |
|---|---|
400 | Invalid/missing body, unsupported extension, or file missing in S3 on complete |
403 | Missing required scope |
404 | Project not accessible / not found, or unknown wav_key |
429 | Rate limit exceeded (20 req/min per API key on these endpoints) |
End-to-End Example (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))