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 kind | Auth flow | Lifetime | Reaches |
|---|---|---|---|
| Partner token | OAuth2 client_credentials, per-team | 1h (refreshable) | partner, ipaas, batch, meta |
| Widget instance token | Back-channel exchange via /v2/partner/widget-instances/{id}/tokens | 1h | widget only |
| Recipient session token | Set in widget bootstrap; cookie-scoped to one journey | Journey lifetime | widget 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.