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.

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

ValueExampleKeep secret?
Client IDcid-mrv_20672c99d4a192fb...No — this is public
Secret Keysk-mrv_443c348c0fc307fa...Yes — shown once, save immediately
Save your secret key

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.

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'])")

You'll get back:

json
{
  "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:

bash
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.

ScopeGrants
project:readList projects — required by every import flow to resolve a project_id
project:writeCreate a project
file:writeFile import — initialize, complete, status
survey:writeSurvey/CSV import — including list surveys; there is no separate read-only survey scope
insight:writeInsight import — initialize, complete, status
mcp:readMCP 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#

StatusErrorCause
400invalid_requestMissing client_id or client_secret
401invalid_clientWrong secret, revoked key, expired key, or suspended user
400invalid_scopeRequested a scope the key doesn't have
429rate_limitedToo many exchanges (limit: 60/min per key)

For failures after the token is issued, see Troubleshooting.

Quick reference#

SettingValue
Token endpointPOST https://app.heymarvin.com/api/v1/oauth/token
MCP serverhttps://mcp.heymarvin.com/
Grant typeclient_credentials
MCP scope + resourcemcp:read · https://mcp.heymarvin.com
Token lifetime1 hour
Rate limit (token exchange)60/min per key