Skip to content

OAuth scopes

Survey Coder Connect uses granular scopescoding:write rather than the legacy umbrella write. This lets partners ask for exactly the access they need (and lets end-customers see exactly what they’re granting on the consent screen).

Legacy scp_live_* keys still use the old read/write/import triad. The backend transparently maps both schemes — endpoints with requireScope('coding:write') accept either a Connect Bearer with that exact granular scope OR a legacy key with write.

ScopeAllows
projects:readList + read projects, questions, responses, and codebooks
projects:writeCreate / update / delete projects, questions, responses
coding:writeRun coding jobs, post /v1/code, generate codebooks
refinement:readList refinement suggestions
refinement:applyApply suggestions, bulk-apply, resolve, undo, export
analytics:readCross-tab, segments, executive summaries, anomalies, all /v2/analytics/*
connections:readList platform connections (SurveyMonkey, Typeform, Qualtrics, etc.)
connections:writeCreate / test / delete platform connections
webhooks:manageRegister, list, delete webhook endpoints
connect:manageExpress-only. Create / list / delete managed sub-orgs. Not available to Standard flow.

In Standard flow, the partner specifies scopes in the consent URL:

https://surveycoder.io/oauth/authorize
?client_id=cc_talk2data
&redirect_uri=https://app.talk2data.io/oauth/callback
&scope=projects:read+coding:write+refinement:apply
&state=<opaque>
&response_type=code

The end-customer sees these scopes in plain language on the consent screen. They can approve or deny but cannot pick a subset — it’s all-or-nothing per consent.

In Express flow, the partner specifies scopes in the client_credentials token request:

Terminal window
curl -X POST https://api.surveycoder.io/oauth/token \
-d grant_type=client_credentials \
-d client_id=cc_talk2data \
-d client_secret=cs_xxxx \
-d "scope=connect:manage coding:write refinement:apply"

The partner can only request scopes that are in their oauth_clients.allowed_scopes list (set when Survey Coder registers the partner).

When a request uses a legacy scp_live_* API key, the middleware maps the key’s old scope to a set of granular scopes:

  • projects:read
  • refinement:read
  • analytics:read
  • connections:read
  • projects:read
  • projects:write
  • coding:write
  • refinement:read
  • refinement:apply
  • analytics:read
  • connections:read
  • connections:write
  • webhooks:manage
  • connections:read
  • connections:write

Every endpoint calls requireScope('<granular>') middleware. The check works like this:

1. If req.oauth (Connect Bearer):
pass when req.oauth.scopes.includes('<granular>')
2. Else if req.apiKey (legacy):
pass when one of req.apiKey.scopes covers '<granular>' via the map above
3. Else:
fail with 403 INSUFFICIENT_SCOPE

The INSUFFICIENT_SCOPE error envelope includes the required scope so you can diagnose:

{
"success": false,
"error": {
"code": "INSUFFICIENT_SCOPE",
"message": "Required scope: refinement:apply",
"doc_url": "https://docs.surveycoder.io/errors/insufficient-scope/",
"request_id": "req_..."
}
}

Picking the right scopes for your integration

Section titled “Picking the right scopes for your integration”

Common combinations:

  • “Read-only analytics dashboard”: projects:read, analytics:read.
  • “Code on behalf of customer”: projects:read, coding:write, refinement:read.
  • “Code + auto-apply refinement”: projects:read, coding:write, refinement:read, refinement:apply.
  • “Full workflow including imports”: projects:read, projects:write, coding:write, refinement:read, refinement:apply, connections:read, connections:write.
  • Express partner: add connect:manage to whatever subset above.

Ask only for what you need — the consent screen is more compelling when the scope list is shorter.