Entitlements
Whether a workspace may use this application at all, and on what plan - the two checks worth making on every request.
Two questions, two endpoints - plus a third the SDK derives from the second rather than a call of its own:
- "May this workspace be here at all?" -
GET /onboarding/workspaces/{workspaceId}. Returnsreadyplus typedblockers, each with acode, aseverity, and aresolveUrlinto the Connect screen that fixes it. Route a blocked user there rather than refusing them with nowhere to go. - "What are they paying for?" -
GET /subscriptions/workspaces/{workspaceId}. Returns the live plan, includingplan.features- the array your backend gates plan-restricted modules on - andplan.limits, the numeric ceilings behind the next question.
Branch on blockers[].code, never on message. Treat an unrecognised code as blocking: the list grows, and a new gate should fail closed.
const readiness = await connect.entitlements.getReadiness(workspaceId);
if (!readiness.ready) return showBlocked(blockingBlockers(readiness)[0]);
if (await connect.entitlements.hasFeature(workspaceId, "campaign-module")) { /* ... */ }- "How much of a metered resource may they have?" -
connect.entitlements.getLimit(workspaceId, key), the numeric counterpart ofhasFeaturefor a resource your own application counts (sites, projects, pipelines). It makes no separate request - it reads the same subscription response above and folds in whatever add-ons raise the plan's own number, so you never join the plan against the add-on list yourself.canAddis the comparison against a count you already have:
if (!(await connect.entitlements.canAdd(workspaceId, "sites", sites.length))) {
return refuse("Your plan's site limit is reached.");
}Endpoints
GET /api/v1/application/subscriptions/workspaces/{workspaceId}
Re-check a workspace's plan & subscription
Scope subscription:read · Rate limit 60 requests / min per key
On-demand counterpart of the SSO token exchange's subscription field - same "re-check without another sign-in" shape as rbac:read above, for billing instead of roles.
Implementation
1. Prerequisite - an API key scoped to subscription:read.
2. Send the request:
curl "https://connect.cosmoops.com/api/v1/application/subscriptions/workspaces/WORKSPACE_ID" \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"3. Branch on hasActiveSubscription - a ready-to-use boolean so you don't need to know the full subscription status enum just to gate access. subscription/plan are both null if the workspace never subscribed - not an error.
4. Gate a plan-restricted feature yourself off plan.features, e.g. plan.features.includes("campaign-module"). Connect only stores which feature keys a plan unlocks; it never enforces them against your requests, so this check has to live in your own backend.
Errors
forbidden (403 - key missing subscription:read), rate_limited (429 - same read-heavy generous limit as rbac:read).
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
workspaceId | path | yes |
Responses
| Status | Meaning |
|---|---|
200 | Subscription + plan detail |
403 | forbidden - key missing subscription:read |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
workspaceId | string | yes | |
applicationId | string | yes | |
hasActiveSubscription | boolean | yes | |
subscription | any | yes | |
plan | any | yes |
GET /api/v1/application/subscriptions
List the workspaces subscribed to your application
Scope subscription:read · Rate limit 10 requests / min per key
The inverse of the per-workspace read above: that one answers about a workspace you already know, this one tells you who your subscribers are. It is what a partner dashboard, an MRR total or a churn report is built on.
Implementation
1. Prerequisite - an API key scoped to subscription:read (the same scope as the per-workspace read).
2. Send the request:
curl "https://connect.cosmoops.com/api/v1/application/subscriptions?status=active&limit=50" \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"3. Page with nextBefore. Each response carries a cursor; pass it back as before for the next page. Stop when it is null - that is the only reliable end-of-list signal, since a full page can still be the last one. The SDK's entitlements.iterateSubscribers() does this for you.
4. Your application is resolved from the API key, so there is no applicationId parameter and no way to ask about anyone else's subscribers.
Not a hot path. This is rate-limited to 10/min per key against the per-workspace read's 60, because it walks your whole subscriber base. Gate a request on /subscriptions/workspaces/{workspaceId}; use this one to build reports.
Errors
validation_error (400 - unknown status, unparseable before, limit outside 1..100), forbidden (403 - key missing subscription:read), rate_limited (429).
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
status | query | no | |
before | query | no | |
limit | query | no |
Responses
| Status | Meaning |
|---|---|
200 | One page of subscribers |
400 | validation_error |
403 | forbidden - key missing subscription:read |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
items | array | yes | |
nextBefore | any | yes |
GET /api/v1/application/onboarding/workspaces/{workspaceId}
Check whether a workspace is cleared to use your application
Scope onboarding:read · Rate limit 60 requests / min per key
Every gate Connect owns between a workspace and your application, resolved in one call: subscription state, KYC/KYB verification, a chargeable payment method, and seat headroom. Broader than subscription:read, which answers only the billing half and can't see the compliance or payment-method gates.
Implementation
1. Prerequisite - an API key scoped to onboarding:read.
2. Send the request:
curl "https://connect.cosmoops.com/api/v1/application/onboarding/workspaces/WORKSPACE_ID" \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"3. Gate on ready - a single boolean, true when nothing blocking remains. Prefer it over inspecting blockers yourself: when Connect adds a new gate, ready accounts for it without any change on your side.
4. Show blockers when ready is false. Each carries a message safe to display as-is and, where one exists, a resolveUrl pointing at the Connect screen that actually fixes it - send the user there rather than dead-ending them. Treat code as a growing set: handle the ones you care about and fall back to message for the rest, so a newly added code never breaks your integration.
5. Note severity - blocking entries are why ready is false; advisory ones (e.g. no saved payment method while a subscription is still active) are worth warning about but don't stop anyone working today.
A workspace that doesn't exist comes back as a normal not-ready response carrying workspace_not_found, not a 404 - so this endpoint can't be used to probe which workspace ids are real.
Errors
forbidden (403 - key missing onboarding:read), rate_limited (429 - same read-heavy limit as subscription:read).
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
workspaceId | path | yes |
Responses
| Status | Meaning |
|---|---|
200 | Readiness verdict + blockers |
403 | forbidden - key missing onboarding:read |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
workspaceId | string | yes | |
applicationId | string | yes | |
ready | boolean | yes | |
readiness | object | yes |
POST /api/v1/application/usage
Report usage against your plan limits
Scope usage:report · Rate limit 120 requests / min per key
Tells Connect how many of your own limited resources a workspace currently has, so the customer's Usage page shows "640 of 1,000 AI actions" instead of a bare ceiling. The limits are the ones you declared in your application's limit catalog and valued per plan (Plan.limits) - the same numbers GET /subscriptions/workspaces/{workspaceId} hands you and entitlements.getLimit() reads.
Implementation
1. Prerequisite - an API key scoped to usage:report, a limit catalog with at least one key, and a workspace subscribed to your application.
2. Send the current count, not a change. Each value is a gauge: the absolute number the workspace has right now - exactly what you already pass to entitlements.canAdd(workspaceId, key, currentCount). Sending it twice is harmless and a lost report is healed by the next one, so there is no Idempotency-Key here. Report after anything that changes a count (create, delete, import), or on a schedule - whichever you already have.
curl -X POST https://connect.cosmoops.com/api/v1/application/usage \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT" \
-H "Content-Type: application/json" \
-d '{"workspaceId":"WORKSPACE_ID","counts":{"records":1840,"ai_actions":640}}'3. Keys you leave out keep their last value. Report one dimension without erasing another. The response carries every count on record for that workspace, each with reportedAt - the customer is shown how old it is.
Connect enforces nothing on these numbers. Your code still refuses the next resource against its live count; this only lets the customer see where they stand without opening your product.
Errors
validation_error (400 - a key not in your limit catalog, named in the message; an empty counts; a cancelled subscription), forbidden (403 - key missing usage:report), not_found (404 - the workspace has no subscription to your application), rate_limited (429).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
workspaceId | uuid | yes | |
counts | object | yes |
Responses
| Status | Meaning |
|---|---|
200 | Every count on record for this workspace |
400 | validation_error - unknown limit key, empty counts, or cancelled subscription |
403 | forbidden - key missing usage:report |
404 | not_found - no subscription to this application |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
workspaceId | string | yes | |
counts | object | yes |
GET /api/v1/application/apps
List the apps a workspace can open, and the ones it could
Scope apps:read · Rate limit 30 requests / min per IP, 60 requests / min per API key
Every active application on the platform, split by subscribed - the data behind a cross-application launcher grid.
subscribed: true is what this workspace can open right now. A paused subscription (a payment retrying) is still one of them, flagged needsAttention rather than hidden - a dunning cycle should not make the product look deleted. Your own application is included and flagged isCurrent, so the grid can render it as the active tile.
subscribed: false is the rest of the catalogue, minus anything this workspace's type is outside the audience of. Send those to {connect}/apps/{applicationId}, where a plan is chosen - not to launchUrl.
This changed in 0.20.0. It used to answer with subscriptions alone; filter on subscribed to get that set back.
<ConnectAppsGrid> in @cosmoops/connect-sdk/client renders this for you.
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
workspaceId | query | yes | The workspace to list apps for. |
Responses
| Status | Meaning |
|---|---|
200 | Launchable apps for this workspace |
400 | validation_error - workspaceId missing |
403 | forbidden - key unknown or missing the apps:read scope. key_revoked / key_expired if the key itself is dead |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
workspaceId | string | yes | |
apps | array | yes |