Access and authorization
Every Marvin API call is authenticated with a short-lived access token. You create an API key once in the Marvin UI, then exchange that key for a token whenever you need one. No browser login is involved, so this works from servers, scripts, and CI.
Create key (once) → Exchange for token (~hourly) → Call the API (every request)
Step 1: Create an API key#
Go to Settings → MCP in the Marvin web app and click Create token. You'll get two values:
If you don't have access to Marvin, please ask an Admin to generate the token for you. The user must be an Admin on a Pro or Enterprise plan to generate these details.
| Value | Example | Keep secret? |
|---|---|---|
| Client ID | cid-mrv_20672c99d4a192fb... | No — this is public |
| Secret Key | sk-mrv_443c348c0fc307fa... | Yes — shown once, save immediately |
The secret key is shown only once. If you lose it, revoke the key and create a new one.
Step 2: Exchange credentials for an access token#
POST /api/v1/oauth/token with grant_type=client_credentials and the scopes you need.
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'])")
You'll get back:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "project:read file:write"
}
The access_token is a signed JWT valid for 1 hour. Cache it and reuse it for its full lifetime rather than exchanging per request — see Token lifecycle and Rate limits.
Step 3: Call the API#
Send the token as a Bearer header on every request:
curl -s "$BASE_URL/api/v1/developer/import/projects" \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
Scopes#
Request only the scopes the job needs. A token is rejected with 403 if it's missing a scope the endpoint requires, and the token exchange itself fails with invalid_scope if you request a scope the key doesn't have.
| Scope | Grants |
|---|---|
project:read | List projects — required by every import flow to resolve a project_id |
project:write | Create a project |
file:write | File import — initialize, complete, status |
survey:write | Survey/CSV import — including list surveys; there is no separate read-only survey scope |
insight:write | Insight import — initialize, complete, status |
mcp:read | MCP server access |
Combine them in one space-separated scope value, e.g. scope=project:read project:write survey:write.
MCP tokens additionally require a resource parameter naming the MCP server you're targeting. See Client configuration.
Team keys vs personal keys#
Project visibility follows the key: team-shared keys see team projects, personal keys see the projects their user can access. A token that returns an empty project list usually means the projects aren't shared with the team.
Token exchange errors#
| Status | Error | Cause |
|---|---|---|
| 400 | invalid_request | Missing client_id or client_secret |
| 401 | invalid_client | Wrong secret, revoked key, expired key, or suspended user |
| 400 | invalid_scope | Requested a scope the key doesn't have |
| 429 | rate_limited | Too many exchanges (limit: 60/min per key) |
For failures after the token is issued, see Troubleshooting.
Quick reference#
| Setting | Value |
|---|---|
| Token endpoint | POST https://app.heymarvin.com/api/v1/oauth/token |
| MCP server | https://mcp.heymarvin.com/ |
| Grant type | client_credentials |
MCP scope + resource | mcp:read · https://mcp.heymarvin.com |
| Token lifetime | 1 hour |
| Rate limit (token exchange) | 60/min per key |