Auth & Scopes

OAuth2 client_credentials, the scope grammar, and how the surface boundary enforces it.

Token kinds

Three kinds of token each consume one surface only. Cross-surface composition happens client-side or via webhook subscriptions, not by URL traversal.

Token kindAuth flowLifetimeReaches
Partner tokenOAuth2 client_credentials, per-team1h (refreshable)partner, ipaas, batch, meta
Widget instance tokenBack-channel exchange via /v2/partner/widget-instances/{id}/tokens1hwidget only
Recipient session tokenSet in widget bootstrap; cookie-scoped to one journeyJourney lifetimewidget only

Scope grammar

Scopes are strings shaped as <surface>:<resource>:<action>. A token carries an array of scopes; endpoints declare required scopes in OpenAPI; the router checks them at the surface boundary.

partner:contacts:read
partner:contacts:write
partner:contacts:delete
partner:cohorts:execute
widget:journey:render
widget:events:write
ipaas:operations:execute
batch:operations:write
meta:capabilities:read

Wildcards. The token MAY carry wildcards like partner:contacts:* or partner:*:read — but these are opt-in per token, and most tokens are issued with explicit scopes.

The bearer header

Authorization: Bearer <token>

No query-string tokens. No signed-URL auth on the partner API. Tokens flow exclusively through the Authorization header.

The shadow-API closure model

The v1 API had a "shadow API" problem: many endpoints existed at /v1/... that no scope check ever sealed off. Any valid v1 partner token could call them. v2 closes this by enforcing scope checks at the router pipeline boundary — before the controller handler runs.

A token that's missing the required scope returns 401 Unauthorized with error code scope_missing:

{
  "errors": [
    {
      "code": "scope_missing",
      "title": "Required scope not present",
      "detail": "This endpoint requires partner:contacts:delete",
      "meta": { "required_scope": "partner:contacts:delete" }
    }
  ]
}

OAuth2 client_credentials flow

Partner tokens are minted via OAuth2 client_credentials:

POST /oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=<your-client-id>
&client_secret=<your-client-secret>
&scope=partner:contacts:read partner:templates:read

→ 200 OK
{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "partner:contacts:read partner:templates:read"
}

The scope param is space-separated. The server returns the subset of requested scopes that your client_id is authorized for. If you request a scope your client isn't entitled to, it's silently dropped from the issued token (your client's contract defines the entitlement ceiling).

Capability discovery

Once you have a token, introspect what it can reach:

GET /v2/meta/capabilities
Authorization: Bearer <token>

→ 200 OK
{
  "surfaces": ["partner", "batch", "meta"],
  "scopes": ["partner:contacts:read", "partner:templates:read"],
  "endpoints": [{ "method": "GET", "path": "/v2/partner/contacts", "required_scope": "partner:contacts:read" }, ...],
  "deprecations": [...],
  "rate_limits": { "class": "partner-tier-1", "limits": [...] }
}

This makes v2 self-describing — a fresh agent or partner can learn the full available surface in one call.