Access
What a signed-in member may do on this application: their resolved permissions, and the role catalog those come from.
Two endpoints, and the difference between them is the whole model.
GET /rbac/users/{userId}- one member's resolved permissions for this application. Connect has already flattened their role, the implicitadminsynthesis and any workspace-scoped custom role into one list.GET /roles- the catalog itself: the roles and permission ids you declared, not anybody's result.
You call the first on requests; you call the second when rendering a role picker.
const { granted } = await connect.access.getUserRoles(userId, { workspaceId });
if (!granted.includes("pulse_email:campaign:manage")) {
return new Response("Forbidden", { status: 403 });
}Connect never enforces these. It resolves them and hands them over; your backend is the only thing standing between a user and the action. Hiding a button is a courtesy, the check behind it is the control.
Permission ids are namespaced by your application key (<key>:<resource>:<action>) and are yours to define - see the Authorization section of your application in the Console.
Endpoints
GET /api/v1/application/rbac/users/{userId}/roles
Re-check a member's role for this application
Scope rbac:read · Rate limit 60 requests / min per key
On-demand counterpart of the SSO token exchange's role/permissions fields (see /sso/token above) - the same resolution, callable anytime instead of only at login.
When to use it
- Your own session outlives Connect's, so the login-time
role/permissionssnapshot has gone stale. - A platform admin changed this member's role mid-session and you want to react immediately rather than waiting for their next sign-in.
Implementation
1. Prerequisite - an API key scoped to rbac:read.
2. Send the request with the Connect userId (from id_token's sub claim) and the workspace:
curl "https://connect.cosmoops.com/api/v1/application/rbac/users/USER_ID/roles?workspaceId=WORKSPACE_ID" \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"3. Use role/granted exactly like role/permissions at login - granted lists this member's permission ids from your Permission Catalog; role is "admin", the key of one of your defined roles, or null if their granted set doesn't exactly match any single defined role.
Errors
validation_error (400 - missing workspaceId), forbidden (403 - key missing rbac:read), rate_limited (429 - generously limited since this is read-heavy by design, a partner may re-check on every one of its own requests).
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
userId | path | yes | |
workspaceId | query | yes |
Responses
| Status | Meaning |
|---|---|
200 | Resolved role |
400 | validation_error - missing workspaceId |
403 | forbidden - key missing rbac:read |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
userId | string | yes | |
workspaceId | string | yes | |
role | any | yes | |
granted | array | yes |
GET /api/v1/application/roles
List this application's role catalog
Scope roles:read · Rate limit 60 requests / min per key
Lists this application's own grantable-role definitions - its Permission Catalog, plus every role that grants a subset of it: a synthesized "admin" entry (always full catalog access) and every role you've defined in the Integration tab. This is what rbac:read resolves a specific member against, not a specific member's result - use rbac:read above for that.
Implementation
1. Prerequisite - an API key scoped to roles:read.
2. Send the request:
curl "https://connect.cosmoops.com/api/v1/application/roles" \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"3. Read permissionCatalog/roles - roles always includes "admin" first; permissionCatalog is empty until at least one permission is defined in the Integration tab.
Errors
forbidden (403 - key missing roles:read), rate_limited (429 - same read-heavy generous limit as rbac:read/subscription:read).
Responses
| Status | Meaning |
|---|---|
200 | Role catalog |
403 | forbidden - key missing roles:read |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
applicationId | string | yes | |
applicationKey | any | yes | |
permissionCatalog | array | yes | |
roles | array | yes |
POST /api/v1/application/oauth/token
Get a token to call another application
Scope applications:call · Rate limit 300 requests / min per API key
Ask Connect for a short-lived token (a JWT, five minutes) to call another application's API for a workspace, then send it to that application as Authorization: Bearer. The provider verifies it locally against /.well-known/jwks.json - Connect is on the token path, never the data path.
Authenticate with your API key as Authorization: Bearer, exactly like every other endpoint here. The body is RFC 6749 form encoding (application/x-www-form-urlencoded; JSON is accepted too).
Two grants
grant_type | Use | Token sub |
|---|---|---|
client_credentials + workspace_id + resource | Background work - syncs, crons, webhooks | your application |
urn:ietf:params:oauth:grant-type:token-exchange + subject_token (a member's SSO session token, issued to you) + resource | A member pressed something that needs the other app's data | the member, with act.sub = your application |
resource is the provider's audience, exactly as it publishes it. scope is optional and space-separated; omitted, you get every scope you are entitled to.
What you get is bounded four ways
The scopes you declared you consume (Console-reviewed) ∩ what the workspace granted on its connections screen ∩ what the provider exposes - and the workspace must subscribe to both applications. A delegated token additionally requires every scope to be delegable and the member to be allowed into both applications.
Cache it
Reuse a token until about a minute before expires_in runs out. connect.applications.client(...) in the SDK does this for you, single-flight.
Wire format
RFC 6749 throughout, not this API's usual envelope: success is §5.1 snake_case (access_token, token_type, expires_in, scope) with Cache-Control: no-store, and a refusal is §5.2's flat { "error", "error_description" }. A bad or under-scoped API key is invalid_client. A 429 keeps the shared envelope and its Retry-After.
Errors
invalid_request (400 - malformed body or unknown grant_type), invalid_client (401/403 - key missing, unknown, revoked, or without applications:call), invalid_target (resource names no provider), invalid_scope (asked beyond what you are entitled to), access_denied (no active connection, or a subscription is not active), invalid_grant (the subject_token is unknown, expired, or not issued to you), unauthorized_client.
Request body
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
{
"type": "object",
"properties": {
"grant_type": {
"type": "string",
"const": "client_credentials"
},
"workspace_id": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"resource": {
"type": "string",
"minLength": 1,
"maxLength": 300
},
"scope": {
"type": "string",
"maxLength": 2000
}
},
"required": [
"grant_type",
"workspace_id",
"resource"
]
},
{
"type": "object",
"properties": {
"grant_type": {
"type": "string",
"const": "urn:ietf:params:oauth:grant-type:token-exchange"
},
"subject_token": {
"type": "string",
"minLength": 1,
"maxLength": 500
},
"subject_token_type": {
"type": "string",
"const": "urn:ietf:params:oauth:token-type:access_token"
},
"resource": {
"type": "string",
"minLength": 1,
"maxLength": 300
},
"scope": {
"type": "string",
"maxLength": 2000
}
},
"required": [
"grant_type",
"subject_token",
"subject_token_type",
"resource"
]
}
]
}Responses
| Status | Meaning |
|---|---|
200 | A token for the provider |
400 | invalid_request / invalid_target / invalid_scope / invalid_grant |
401 | invalid_client - no usable API key |
403 | invalid_client / access_denied / unauthorized_client |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
access_token | string | yes | |
token_type | string | yes | |
expires_in | integer | yes | |
scope | string | yes | |
issued_token_type | string | no |
POST /api/v1/application/events/publish
Publish an event to connected applications
Scope events:publish · Rate limit 300 requests / min per API key
Publish one of the events your application declared (Console > Integration > Exposed API) for a workspace. Connect delivers it to every application that workspace connected to you with the event's required scope, and that subscribed its webhook endpoint to application.event - signed like every other Connect webhook, retried, and logged.
Not the analytics feed: POST /events counts your product events, this one delivers business events to other applications.
Idempotency
Send idempotencyKey in the body or an Idempotency-Key header (if you send both, they must match). A repeat within 24 hours answers 202 with deduplicated: true and delivers nothing new, so retrying a timeout is always safe. The event id is derived from the key, so a retry names the same event and consumers dedupe on it.
202 means queued
Acceptance means the fan-out is queued, not that any consumer has the event. You never name the recipients - only the workspace's connections decide who receives it.
Errors
validation_error (400 - malformed body, an undeclared type, or two different idempotency keys), forbidden (403 - key unknown, missing events:publish, or that workspace does not subscribe to your application), rate_limited (429).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
workspaceId | string | yes | |
type | string | yes | |
data | object | yes | |
idempotencyKey | string | yes |
Responses
| Status | Meaning |
|---|---|
202 | Accepted for delivery, or a deduplicated repeat |
400 | validation_error |
403 | forbidden |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | |
deduplicated | boolean | yes |
GET /api/v1/application/connections
List a workspace's connections to your application
Scope applications:call · Rate limit 300 requests / min per API key
Every connection in the named workspace that your application is a party to - as the consumer (you call the other application) or the provider (it calls you). This is what your settings page reads to say "Connected", and what a background job reads to decide whether it can ask for a token.
scopes is the effective set: what the workspace granted, still declared by the consumer and still exposed by the provider - exactly what a token can carry today. A revoked connection is listed with status: "revoked" and no scopes, so a page can say "Disconnected" rather than "never connected".
pendingScopes is what a reconnect would add: the scopes the consumer declares and the provider exposes now, minus what the workspace granted. It is non-empty when the consumer has widened its declaration since the grant - the connection still works at the old scope, and the page should say "Needs reconnecting" and link the admin to /apps/connect?consumer=&provider=&mutual=1. Always empty for a revoked connection and when the grant is up to date.
Each connection is one direction (consumer → provider). Two applications that call each other hold two records; a settings page that shows one "Connect" for the pair should read both, and treat the pair as connected only when every direction it needs is active with no pendingScopes.
Connections between two other applications in the same workspace are never listed.
Errors
validation_error (400 - workspaceId missing), forbidden (403 - key unknown or missing applications:call), rate_limited (429).
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
workspaceId | query | yes |
Responses
| Status | Meaning |
|---|---|
200 | The caller's connections in that workspace, active first |
400 | validation_error |
403 | forbidden |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
connections | array | yes |
GET /api/v1/application/connections/providers
List the applications you consume, with their audiences
Scope applications:call · Rate limit 300 requests / min per API key (shared with GET /connections)
Every application your application declares it consumes (Console > Integration > Connections), with the audience a token for it is minted against - the value resource must carry on POST /oauth/token. It lets you address a provider by its key instead of keeping its URL in your own configuration; @cosmoops/connect-sdk's Products relay resolves products this way.
Only your own declarations are listed - an application you do not declare is never named, so this cannot be used to discover another application's audience. audience is null for a declaration with nothing behind it: the provider no longer exists or exposes no API.
It says what you could call, not whether a workspace connected the pair - the token endpoint decides that per workspace on every issuance. The answer changes only when someone edits an application in Console, so cache it for minutes.
Errors
forbidden (403 - key unknown or missing applications:call), rate_limited (429).
Responses
| Status | Meaning |
|---|---|
200 | Every application you declare you consume, in declaration order |
403 | forbidden |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
providers | array | yes |