Skip to content

Workspaces API

Read workspace identity and API entitlement, change workspace settings, manage members and invitations, and issue or revoke API keys.

On this page

A workspace owns every bucket, file, member, and API key. Use these routes to check which workspace a key belongs to and what its plan allows, or to build internal admin tooling that manages members and keys.

Who can call what#

Most routes on this page change who has access to the workspace, so they deliberately accept only a signed-in person, not an API key. An API key cannot create other keys, invite people, or change roles, even if it has the workspace:admin scope. Those requests pass the scope check and then fail with 403 and "code": "insufficient_workspace_permission".

RouteAPI keySigned-in session
GET /api/workspaces, GET /api/workspaces/current, GET /api/workspaces/current/membersworkspace:readAny member
GET /api/workspaces/current/api-entitlementworkspace:readOwner or admin
PATCH /api/workspaces/current, members, invitations, API keysNot allowedOwner or admin
POST /api/workspaces, accept an invitationNot allowedAny signed-in user
DELETE /api/workspaces/current, transfer ownershipNot allowedOwner only

A signed-in session sends Authorization: Bearer <access token>. If the person belongs to more than one workspace, also send X-Workspace-Id; without it the request fails with 400 and "code": "workspace_required". API keys are bound to one workspace and do not need the header. Sending a different workspace ID with a key returns 403. See Authentication for both methods.

Admins can manage members, viewers, and invitations, but only the owner can promote, demote, invite, or remove an admin.

The workspace object#

Workspace
{
  "id": "6e0b2f7a-1c94-4d3b-a8e5-2f71c0d9b6a3",
  "name": "Northwind Studio",
  "pictureUrl": null,
  "role": "admin",
  "permissions": ["analytics:read", "assets:read", "assets:write", "billing:manage", "conversions:run", "workspace:admin", "workspace:read"],
  "bucketCount": 6,
  "encryptionEnabled": false,
  "settings": {
    "defaultVisibility": "private",
    "requireCleanDelivery": true,
    "accentTheme": "steadylink"
  },
  "plan": "pro",
  "billingStatus": "active"
}

Fields

rolestring
The caller's role: owner, admin, member, or viewer. For an API key it is api_key, and permissions is empty because a key's access comes from its scopes.
permissionsstring[]
What the caller's role allows in this workspace.
bucketCountinteger
Number of buckets. Only GET /api/workspaces counts them; the single-workspace routes always return 0.
encryptionEnabledboolean
Whether newly committed originals are encrypted. See Encryption.
settingsobject
Workspace defaults. defaultVisibility (default private) and requireCleanDelivery (default true) are always present. The object can also hold keys managed by other routes, such as deliveryPolicy.
planstring
Plan identifier, for example hobby, personal, pro, or business.

Workspaces#

List workspaces#

GET/api/workspaces
Requiresworkspace:read

For a signed-in user, returns every workspace they belong to, sorted by name. For an API key, returns only the key's workspace.

curl
curl https://api.steadylink.io/api/workspaces \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "items": [
    {
      "id": "6e0b2f7a-1c94-4d3b-a8e5-2f71c0d9b6a3",
      "name": "Northwind Studio",
      "role": "api_key",
      "permissions": [],
      "bucketCount": 6,
      "...": "other workspace fields"
    }
  ]
}

Get the current workspace#

GET/api/workspaces/current
Requiresworkspace:read

Returns the workspace the key belongs to, or the one selected with X-Workspace-Id. A quick way to confirm which workspace a key points at before running a script against it.

curl
curl https://api.steadylink.io/api/workspaces/current \
  -H "X-API-Key: $STEADYLINK_API_KEY"

The response is a workspace object with status 200 OK.

Get the API entitlement#

GET/api/workspaces/current/api-entitlement
Requiresworkspace:read

Returns whether developer API access is active for the workspace, why not if it is not, and the limits that apply to every key in the workspace. All keys share these limits; creating more keys does not add capacity.

A signed-in owner or admin can read this even while API access is paused, which is how the dashboard explains a paused state. An API key cannot: while access is paused, every key request, including this one, fails with 403 before it reaches the route.

curl
curl https://api.steadylink.io/api/workspaces/current/api-entitlement \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "enabled": true,
  "reason": null,
  "plan": "pro",
  "billingStatus": "active",
  "currentPeriodEnd": "2026-11-01T00:00:00",
  "limits": {
    "readRequestsPerMinute": 1200,
    "writeRequestsPerMinute": 300,
    "burstRequestsPer10Seconds": 300,
    "expensiveOperationsPerMinute": 120,
    "expensiveOperationsPerDay": 25000,
    "requestsPerMonth": 2000000,
    "activeApiKeys": 10,
    "maxUploadBytes": 5368709120
  },
  "activeApiKeys": 2
}

Response fields

enabledboolean
Whether API keys work for this workspace right now.
reasonstring | null
Why access is off: subscription_inactive (a paid plan whose billing is not active or whose period has ended), paid_plan_required, or api_access_disabled (turned off for the workspace by SteadyLink). null when enabled.
limitsobject | null
Per-plan limits for the whole workspace. activeApiKeys inside limits is the maximum; the top-level activeApiKeys is how many unrevoked, unexpired keys exist now.

The free plan includes API access with one active key and 50,000 requests a month. For how these limits are applied and what a 429 looks like, see Usage and limits.

Update workspace settings#

PATCH/api/workspaces/current
Signed-in session (owner or admin)

Changes the workspace name, settings, or both. Send only what you want to change. Settings you omit keep their current values.

Body

namestring
New name, 1 to 200 characters.
settings.defaultVisibilitystring
public or private. The default visibility for new uploads. A bucket can override it. Workspaces with no stored value behave as private.
settings.requireCleanDeliveryboolean
A stored workspace preference. Any value is saved as a boolean.
settings.accentThemestring
Dashboard accent: steadylink, lime, iris, sunset, or sky.
curl
curl -X PATCH https://api.steadylink.io/api/workspaces/current \
  -H "Authorization: Bearer $STEADYLINK_ACCESS_TOKEN" \
  -H "X-Workspace-Id: 6e0b2f7a-1c94-4d3b-a8e5-2f71c0d9b6a3" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Northwind Studio", "settings": { "defaultVisibility": "public" } }'

The response is the updated workspace object. Any other key inside settings returns 422 with "code": "invalid_settings" and the rejected keys in invalid.

Create a workspace#

POST/api/workspaces
Signed-in session

Creates a workspace on the free plan with the caller as owner. Send { "name": "Client portal" }. Returns 201 Created with the new workspace object.

Delete the current workspace#

DELETE/api/workspaces/current
Signed-in session (owner)

Permanently deletes the workspace, every file and revision in it, and its stored bytes. To confirm, the body must repeat the exact workspace name: { "name": "Northwind Studio" }. A mismatch returns 422.

You cannot delete your only workspace. Create another first, or the request fails with 409 and "code": "last_workspace". On success the response names the workspace to switch to:

200 OKResponse
{ "deleted": true, "nextWorkspaceId": "b8f41d2c-5e60-4a17-93cb-0c2e7d4f1a85" }

Members#

List members#

GET/api/workspaces/current/members
Requiresworkspace:read
curl
curl https://api.steadylink.io/api/workspaces/current/members \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "items": [
    { "userId": "0d9e3a71-48c2-4b5f-a1e6-7c2b9f0e4d38", "name": "Ana Ruiz", "email": "[email protected]", "role": "admin", "avatarUrl": null },
    { "userId": "5b7c1f20-93ad-4e8b-b6f4-2a0d8e1c7f95", "name": "Sam Lee", "email": "[email protected]", "role": "member", "avatarUrl": null }
  ]
}

Change a member's role#

PATCH/api/workspaces/current/members/{user_id}
Signed-in session (owner or admin)

Body

rolestringRequired
admin, member, or viewer.

Returns the updated member. The owner's role cannot be changed this way (409); transfer ownership instead. An admin who tries to change an admin, or to make someone an admin, gets 403.

Remove a member#

DELETE/api/workspaces/current/members/{user_id}
Signed-in session (owner or admin)

Removes the person's access immediately and returns { "removed": true }. The owner cannot be removed (409), and only the owner can remove an admin.

Transfer ownership#

POST/api/workspaces/current/ownership
Signed-in session (owner)

Makes another member the owner and turns the current owner into an admin. Send { "userId": "0d9e3a71-48c2-4b5f-a1e6-7c2b9f0e4d38" }. Returns { "transferred": true, "ownerUserId": "..." }.

Invitations#

Invite a member#

POST/api/workspaces/current/invitations
Signed-in session (owner or admin)

Emails an invitation link to the address. The invitation reserves a seat until it is accepted, revoked, or expires.

Body

emailstringRequired
The invitee's email address. It is matched case-insensitively when they accept.
rolestringDefault member
admin, member, or viewer. Only the owner can invite an admin.
expiresInDaysintegerDefault 7
From 1 to 30.
curl
curl -X POST https://api.steadylink.io/api/workspaces/current/invitations \
  -H "Authorization: Bearer $STEADYLINK_ACCESS_TOKEN" \
  -H "X-Workspace-Id: 6e0b2f7a-1c94-4d3b-a8e5-2f71c0d9b6a3" \
  -H "Content-Type: application/json" \
  -d '{ "email": "[email protected]", "role": "viewer", "expiresInDays": 14 }'
201 CreatedResponse
{
  "id": "e2a7c9d4-0b18-4f63-9c5e-71d4a3b8f026",
  "email": "[email protected]",
  "role": "viewer",
  "status": "pending",
  "expiresAt": "2026-10-22T10:04:31.118204",
  "createdAt": "2026-10-08T10:04:31.118204",
  "token": "q8Vw...",
  "acceptPath": "/api/workspaces/invitations/q8Vw.../accept"
}

token is returned only in this response. Inviting the same address again revokes the earlier pending invitation. Errors: 409 if the person is already a member, 409 with "code": "seat_limit_reached" if members plus pending invitations already fill the plan's seats.

List invitations#

GET/api/workspaces/current/invitations
Signed-in session (owner or admin)

Returns every invitation, newest first, with status of pending, accepted, revoked, or expired. Tokens are never included.

Revoke an invitation#

DELETE/api/workspaces/current/invitations/{invitation_id}
Signed-in session (owner or admin)

Returns { "revoked": true } and frees the reserved seat.

Accept an invitation#

POST/api/workspaces/invitations/{token}/accept
Signed-in session (invitee)

The signed-in user's email must match the invitation. Returns { "accepted": true, "workspaceId": "...", "role": "viewer" }. Errors: 404 if the invitation is not pending, 410 if it expired, 403 if it belongs to another email address.

API keys#

List API keys#

GET/api/workspaces/current/api-keys
Signed-in session (owner or admin)

Returns metadata for every key, newest first, including revoked and expired keys. The raw key value is never returned after creation.

200 OKResponse
{
  "items": [
    {
      "id": "a3c5e7f9-1b2d-4e6f-8a0c-2e4f6a8c0b1d",
      "name": "production uploader",
      "scopes": ["assets:read", "assets:write"],
      "createdAt": "2026-09-14T08:30:00.000000",
      "lastUsedAt": "2026-10-08T09:55:12.402113",
      "expiresAt": "2026-12-13T08:30:00.000000",
      "revokedAt": null
    }
  ]
}

lastUsedAt is updated at most every five minutes, so treat it as approximate.

Create an API key#

POST/api/workspaces/current/api-keys
Signed-in session (owner or admin)

Body

namestringRequired
A label you will recognize later, 1 to 200 characters.
scopesstring[]Default ["assets:read", "assets:write"]
Any of assets:read, assets:write, analytics:read, workspace:read, workspace:admin. Grant only what the integration needs.
expiresInDaysinteger
From 1 to 365. Omit for a key that does not expire.
curl
curl -X POST https://api.steadylink.io/api/workspaces/current/api-keys \
  -H "Authorization: Bearer $STEADYLINK_ACCESS_TOKEN" \
  -H "X-Workspace-Id: 6e0b2f7a-1c94-4d3b-a8e5-2f71c0d9b6a3" \
  -H "Content-Type: application/json" \
  -d '{ "name": "production uploader", "scopes": ["assets:read", "assets:write"], "expiresInDays": 90 }'
201 CreatedResponse
{
  "id": "a3c5e7f9-1b2d-4e6f-8a0c-2e4f6a8c0b1d",
  "name": "production uploader",
  "scopes": ["assets:read", "assets:write"],
  "createdAt": "2026-10-08T10:12:00.000000",
  "lastUsedAt": null,
  "expiresAt": "2027-01-06T10:12:00.000000",
  "revokedAt": null,
  "key": "slk_..."
}

Errors:

StatusCodeMeaning
403subscription_inactive, paid_plan_required, api_access_disabledAPI access is not active for the workspace.
409api_key_limit_reachedThe plan's active key limit is reached. Revoke an unused key first.
422invalid_scopesAn unknown scope, or an empty list. The response lists allowed scopes.

Most people create keys in Dashboard > Developers instead. See API keys for scope choices and rotation.

Revoke an API key#

DELETE/api/workspaces/current/api-keys/{key_id}
Signed-in session (owner or admin)

Revokes the key immediately and returns { "revoked": true }. Requests that use it fail with 401 from then on. Revoked keys stay in the list with revokedAt set.

Next steps#