Troubleshooting
Authentication#
| Problem | Fix |
|---|---|
invalid_client on token exchange | Secret is wrong, key is revoked, key has expired, or the user is suspended. Create a new key if unsure. |
invalid_scope on token exchange | You requested a scope the key doesn't have. See Scopes. MCP keys use mcp:read. |
rate_limited on token exchange | You're exchanging too often. Cache the token for its full 1-hour lifetime and check the Retry-After header. |
401 on an API call | Token has expired (they last 1 hour) — exchange a new one. |
401 from the MCP server | Check that your token includes scope=mcp:read and the matching resource parameter (https://mcp.heymarvin.com or https://mcp-eu.heymarvin.com). |
403 on an API call | The token is valid but missing the scope that endpoint requires. |
| Token works but no projects returned | The key only reaches projects it has access to. Make sure the projects are shared with the team in Marvin. |
403 on create project | Missing project:write, or the team setting allow_manual_project_creation is disabled. |
HTTP status reference#
Applies to the /api/v1/developer/import/* endpoints.
| HTTP | When |
|---|---|
400 | Invalid or missing body, unsupported file extension, files missing in S3 on complete, or an operation not allowed in the current status |
403 | Missing required scope, or project creation disabled for the team |
404 | Project not accessible or not found, or unknown wav_key / upload_key / insight_key |
409 | Conflicting idempotency_key (same key, different API key), or two appends racing on the same upload |
429 | Rate limit exceeded — 20 req/min per API key on import endpoints |
Uploads#
"File not found in S3. Upload the file before calling complete." Complete verifies the object exists before starting processing. Either the S3 upload didn't finish, or it silently failed. Check the S3 response status: a presigned POST returns 204 on success, and a 403 there usually means the URL expired or a required field from upload_fields was omitted.
S3 rejects the upload. Send every entry in upload_fields as a form field, and send them before the file field — S3 evaluates policy fields in order and ignores anything after the file. Also check the size caps: 50 MB per CSV part, 500 MB for documents, images, and insights, 10 GB for audio and video.
Presigned URL expired. URLs live 1 hour. Get fresh ones with retry a part or retry a media file; for files and insights, call initialize again. See Token lifecycle.
Complete returns missing_parts or missing_media_files. Marvin checks every registered part and media file synchronously before enqueueing work, so nothing has started yet. Re-upload exactly what's listed, then call complete again.
Processing#
Survey upload is FAILED after some parts succeeded. Parts process sequentially and independently, and completed parts are not rolled back. Read error_details for the failing part number, call retry on just that part, re-upload it, then call complete again — already-processed parts are skipped. Never retry a part whose status is PROCESSED; it returns 400 because its rows are already saved and re-uploading would duplicate them.
Individual media files ended FAILED but the upload COMPLETED. Linking is per file. A file fails when no linker matched it: an unsupported extension, a filename stem that matches no respondent, a MEDIA_RESPONSE cell value that doesn't exactly equal the registered filename, or the wrong number of delimiter-separated segments. Matching is exact and case-sensitive. See linking modes.
Media column wasn't detected. auto_map: true never assigns MEDIA_RESPONSE — AI mapping only detects Name, Email, Timestamp, and Open-ended. Map media columns explicitly in column_mapping.
Append rejects a part. Appended parts must include a header row with all of the existing survey's question columns; extra columns are ignored rather than imported. Append is only allowed while the upload's status is COMPLETED.
Insight stayed a private draft. should_publish is set on complete, not initialize. Without it the insight stays insight_state: 1.
Duplicates#
Retrying a create can double-import. Pass an idempotency_key to survey initialize — the same key with the same API key returns the existing upload, while the same key with a different API key returns 409.
Insight initialize accepts no idempotency key, so each call creates a new insight. Insight complete is idempotent, however: once the insight reaches completed or failed it returns 200 with the current status, and a duplicate call racing an in-progress one gets 200 with "message": "Upload is already processing; see status endpoint." rather than enqueueing a second job.