API

Members and permissions

Manage organizations, members, invitations, and grants.

The Permissions API runs Openship's team and access-control model: it reports whether the active org is a team, lists the resources you can grant, creates a team organization, and manages the per-member grants and invitations that scope a restricted member to specific projects, servers, or other resources. In the dashboard this is the organization's Team tab. See Auth & access for how roles and grants combine.

User grants give a restricted member access to specific resources. Roles also provide their standard permissions. Token grants and read-only restrictions are an additional limit for every role. A user grant is (user, resourceType, resourceId, permissions[]); resourceId: "*" applies across the organization.

List workspaces

GET /api/permissions/workspaces (SDK: ship.permissions.listWorkspaces()) returns workspace names, organizationId values, slugs, team status, and effective roles, including workspaces with no projects. The response also identifies currentOrganizationId, boundOrganizationId, canSwitchOrganization, and readOnly. It is a membership discovery operation available to every authenticated member, including restricted and read-only credentials.

Organization-bound credentials only see their own workspace. Unbound requests list the user's memberships; an explicitly fixed SDK or HTTP scope returns only its selected workspace. canSwitchOrganization describes whether the credential permits selecting other memberships. The response contains workspace identity metadata, without billing credentials or runtime namespace details.

Through MCP, call get_permissions_workspaces with {}, then pass the chosen organizationId as a top-level argument when creating an app or Compose project. See MCP workspace selection.

Create a team organization

Creates a brand-new organization, marks it as a team, and sets the caller as its owner. Personal workspaces stay personal — this is the way to get a shared, multi-member org.

const result = await ship.permissions.createTeamOrg({
  name: "Acme Platform",
});
console.log(result);
curl -X POST "$OPENSHIP_URL/api/permissions/create-team-org" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Acme Platform"}'

Prop

Type

Returns 201 with { data: { id, name, isTeam: true } }.

List grantable resources

Side-effect-free picker payload for the grant modal and invite flow. Pass the resource type; get back { id, label, meta? }[]. The * wildcard is not listed — the picker adds it itself.

const result = await ship.permissions.listResources({
  type: "server",
});
console.log(result);
curl "$OPENSHIP_URL/api/permissions/resources?type=server" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN"

For type=billing or type=audit the only entry is the org-wide *. For type=github_repository you can narrow to one account with &owner=<login>.

Grant a resource

Idempotent upsert of a single grant tuple. Re-sending the same (userId, resourceType, resourceId) replaces its permissions in place. An empty permissions array revokes the grant (Openship keeps no zero-permission placeholder rows).

const result = await ship.permissions.upsertGrant({
  userId: "user_123",
  resourceType: "project",
  resourceId: "proj_abc",
  permissions: ["read", "write"],
});
console.log(result);
curl -X POST "$OPENSHIP_URL/api/permissions/grants" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"userId":"user_123","resourceType":"project","resourceId":"proj_abc","permissions":["read","write"]}'

Prop

Type

Returns 201 with { data: <grant> }, or { data: null, revoked: true } when permissions was empty.

Replace a member's grant set

Sends the member's entire desired grant set; the server diffs against what exists — upserting added/changed tuples and deleting any that are absent. This is the single save path for the member-grants editor. Zero-permission entries are dropped.

const result = await ship.permissions.replaceGrants({
  userId: "user_123",
  grants: [
    {
      resourceType: "project",
      resourceId: "proj_abc",
      permissions: ["read", "write"],
    },
    {
      resourceType: "server",
      resourceId: "srv_1",
      permissions: ["read"],
    },
  ],
});
console.log(result);
curl -X PUT "$OPENSHIP_URL/api/permissions/grants" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "userId":"user_123",
    "grants":[
      {"resourceType":"project","resourceId":"proj_abc","permissions":["read","write"]},
      {"resourceType":"server","resourceId":"srv_1","permissions":["read"]}
    ]
  }'

Prop

Type

Each entry of grants is a Grant object:

Prop

Type

Non-wildcard, row-backed resources are checked to belong to the active org; a foreign id is rejected with 400 RESOURCE_NOT_IN_ORG.

Invite a member with grants

Wraps Better Auth's invite and, in the same call, stores pending grants against the invitation. When the invitee accepts, the accept-invite page calls POST /invitations/:id/materialize to turn those pending grants into real ones.

const result = await ship.permissions.inviteWithGrants({
  email: "dev@acme.com",
  role: "restricted",
  grants: [
    {
      resourceType: "project",
      resourceId: "proj_abc",
      permissions: ["read", "write"],
    },
  ],
});
console.log(result);
curl -X POST "$OPENSHIP_URL/api/permissions/invite-with-grants" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email":"dev@acme.com",
    "role":"restricted",
    "grants":[{"resourceType":"project","resourceId":"proj_abc","permissions":["read","write"]}]
  }'

Prop

Type

Returns 201 with { data: { id, email, role, pendingGrantCount } }.

Materialize an invitation's grants

Called from the accept-invite page after Better Auth records the acceptance. It finds the invitation's pending grants, upserts them as real grants for the now-joined user, and clears the pending rows. Authorized by the invitation itself — the caller's email must match the invite and its status must be accepted.

const result = await ship.permissions.materializeInvitation("inv_123");
console.log(result);
curl -X POST "$OPENSHIP_URL/api/permissions/invitations/inv_123/materialize" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN"

Returns { data: { materialized: <count> } }.

Errors

400 — missing or invalid fields

userId, resourceType, and resourceId are required on POST /grants; name is required on create-team-org; email on invite-with-grants. An unknown resourceType returns code: "INVALID_RESOURCE_TYPE". On the invitation flow, 400 also means the invite hasn't been accepted yet or has expired.

403 — not admin, or wrong invitee

The grant/invitation routes require the caller to be admin or owner in the active org. On /invitations/:id/materialize, 403 means your account's email doesn't match the invitation.

404 — not a member / grant not found

POST /grants returns 404 when the target userId isn't a member of the active org; DELETE /grants/:id returns 404 when the id doesn't belong to your organization (grants are org-scoped, so you can't touch another tenant's rows even by guessing the id).

Operations

OperationSDKREST API
List available workspace IDs, names, roles, and credential scope.permissions.listWorkspaces()GET /api/permissions/workspaces
permissions:read
Read the selected organization’s permission metadata.permissions.orgMeta()GET /api/permissions/org-meta
permissions:read
List resources available for permission grants.permissions.listResources(input)GET /api/permissions/resources
permissions:read
Create a team organization with the caller as its owner.permissions.createTeamOrg(input)POST /api/permissions/create-team-org
permissions:write
Read grants for the requested subject.permissions.listGrants(input)GET /api/permissions/grants
permissions:read
Create or update one resource grant.permissions.upsertGrant(input)POST /api/permissions/grants
permissions:write
Replace the requested subject’s grant set.permissions.replaceGrants(input)PUT /api/permissions/grants
permissions:write
List organization invitations.permissions.listInvitations()GET /api/permissions/invitations
permissions:read
Invite a member with explicit role and optional grants.permissions.inviteWithGrants(input)POST /api/permissions/invite-with-grants
permissions:write
List organization members.permissions.listMembers()GET /api/permissions/members
permissions:read
Apply accepted invitation grants to the resulting membership.permissions.materializeInvitation(id)POST /api/permissions/invitations/:id/materialize
permissions:write
Remove one grant.permissions.deleteGrant(id)DELETE /api/permissions/grants/:id
permissions:admin
Accept an authorized invitation.permissions.acceptInvitation(id)POST /api/permissions/invitations/:id/accept
permissions:write
Reject an invitation.permissions.rejectInvitation(id)POST /api/permissions/invitations/:id/reject
permissions:write
Cancel a pending invitation.permissions.cancelInvitation(id)POST /api/permissions/invitations/:id/cancel
permissions:write
Resend an eligible invitation.permissions.resendInvitation(id, input?)POST /api/permissions/invitations/:id/resend
permissions:write
Change a member’s organization role.permissions.setMemberRole(id, input)PATCH /api/permissions/members/:id
permissions:write
Remove a member while preserving the final owner.permissions.removeMember(id)DELETE /api/permissions/members/:id
permissions:admin

On this page