Update Member
curl --request PATCH \
--url https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"capabilities": [
"<string>"
],
"permissions": [
"<string>"
],
"rank": 123,
"relativeRank": 123,
"title": "<string>",
"metadata": {},
"actingUserId": "<string>"
}
'import requests
url = "https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId"
payload = {
"capabilities": ["<string>"],
"permissions": ["<string>"],
"rank": 123,
"relativeRank": 123,
"title": "<string>",
"metadata": {},
"actingUserId": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.patch(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PATCH',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
capabilities: ['<string>'],
permissions: ['<string>'],
rank: 123,
relativeRank: 123,
title: '<string>',
metadata: {},
actingUserId: '<string>'
})
};
fetch('https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PATCH",
CURLOPT_POSTFIELDS => json_encode([
'capabilities' => [
'<string>'
],
'permissions' => [
'<string>'
],
'rank' => 123,
'relativeRank' => 123,
'title' => '<string>',
'metadata' => [
],
'actingUserId' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId"
payload := strings.NewReader("{\n \"capabilities\": [\n \"<string>\"\n ],\n \"permissions\": [\n \"<string>\"\n ],\n \"rank\": 123,\n \"relativeRank\": 123,\n \"title\": \"<string>\",\n \"metadata\": {},\n \"actingUserId\": \"<string>\"\n}")
req, _ := http.NewRequest("PATCH", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.patch("https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"capabilities\": [\n \"<string>\"\n ],\n \"permissions\": [\n \"<string>\"\n ],\n \"rank\": 123,\n \"relativeRank\": 123,\n \"title\": \"<string>\",\n \"metadata\": {},\n \"actingUserId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Patch.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"capabilities\": [\n \"<string>\"\n ],\n \"permissions\": [\n \"<string>\"\n ],\n \"rank\": 123,\n \"relativeRank\": 123,\n \"title\": \"<string>\",\n \"metadata\": {},\n \"actingUserId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyWorkspace — Membership
Update Member
Edit a member’s access or profile fields, with two capability tiers
Update Member
curl --request PATCH \
--url https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"capabilities": [
"<string>"
],
"permissions": [
"<string>"
],
"rank": 123,
"relativeRank": 123,
"title": "<string>",
"metadata": {},
"actingUserId": "<string>"
}
'import requests
url = "https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId"
payload = {
"capabilities": ["<string>"],
"permissions": ["<string>"],
"rank": 123,
"relativeRank": 123,
"title": "<string>",
"metadata": {},
"actingUserId": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.patch(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PATCH',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
capabilities: ['<string>'],
permissions: ['<string>'],
rank: 123,
relativeRank: 123,
title: '<string>',
metadata: {},
actingUserId: '<string>'
})
};
fetch('https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PATCH",
CURLOPT_POSTFIELDS => json_encode([
'capabilities' => [
'<string>'
],
'permissions' => [
'<string>'
],
'rank' => 123,
'relativeRank' => 123,
'title' => '<string>',
'metadata' => [
],
'actingUserId' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId"
payload := strings.NewReader("{\n \"capabilities\": [\n \"<string>\"\n ],\n \"permissions\": [\n \"<string>\"\n ],\n \"rank\": 123,\n \"relativeRank\": 123,\n \"title\": \"<string>\",\n \"metadata\": {},\n \"actingUserId\": \"<string>\"\n}")
req, _ := http.NewRequest("PATCH", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.patch("https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"capabilities\": [\n \"<string>\"\n ],\n \"permissions\": [\n \"<string>\"\n ],\n \"rank\": 123,\n \"relativeRank\": 123,\n \"title\": \"<string>\",\n \"metadata\": {},\n \"actingUserId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.sublay.io/v7/:projectId/workspaces/:id/members/:userId")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Patch.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"capabilities\": [\n \"<string>\"\n ],\n \"permissions\": [\n \"<string>\"\n ],\n \"rank\": 123,\n \"relativeRank\": 123,\n \"title\": \"<string>\",\n \"metadata\": {},\n \"actingUserId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyEdits a member, with two capability tiers by field sensitivity:
Checked after the offset is resolved. Out-of-range
A schema rejection, so the message is prefixed with the field it was reported against.
A service/master key called this route without naming an
A plain user token sent an
Every path id on the workspaces bundle is checked for UUID shape before the route runs, so a malformed one is a plain
- Powerful fields (
capabilities,permissions, and rank — as eitherrankorrelativeRank) requireedit-member-access, plus the rank rule and the no-escalation guard. - Cosmetic fields (
title,metadata) require onlyedit-member-profile. A member editing their owntitleneeds no capability.
rank (absolute) or relativeRank (an offset from the actor) — and they are mutually exclusive; sending both is a 400. Omitting both means rank unchanged: unlike Create Invite, this endpoint has no value default, so editing someone’s capabilities never moves them on the ladder. See choosing a rank.
Grant vs strip asymmetry: you may only add a capability/permission you hold on the target node (resolved set, including via reach); stripping is governed by rank alone. Cross-node rank is not compared — the rank guard runs only when actor and target share a member row on the same workspace.
Path Parameters
string
required
The workspace UUID.
string
required
The target member’s user id.
Body Parameters
All optional; the controller enforces which tier each touched field belongs to. At least one must be provided.string[]
New capability set (powerful —
edit-member-access).string[]
New permission set (powerful —
edit-member-access).number
New absolute rank,
0 – 2147483647 (powerful — edit-member-access + rank rules). Mutually exclusive with relativeRank. Omitting both leaves rank unchanged.number
New rank expressed as an offset from the acting user:
1 = one rung below me. Must be 1 – 2147483647 — 0 (a peer) and negatives are a 400. The resolved value is bounded too, so an in-range offset that overflows once anchored is a 400, not a 500. Mutually exclusive with rank, and with no default: omitting both leaves rank unchanged.Anchored on the actor’s own rank if they hold a member row on this workspace, apex (one step above rank 0) otherwise — the anchor turns on the row, not on what kind of actor they are. Resolved to an absolute number at write time and stored absolute; it is a snapshot and does not follow the actor’s own rank afterwards.Because an in-ladder anchor is >= 0 and the offset is >= 1, a direct-member actor can never reach rank 0 this way — and neither can they by naming absolute rank: 0, which the assign rule below refuses for exactly the same reason (nothing is strictly below 0). There is no separate rank-0 guard: rank 0 is effectively owner-only because the owner skips the rank block, not because a check names them. An actor with no member row here — an ancestor owner, or a cross-node reach holder — also skips it.A service/master key is not on that list. Unlike Create Invite, this route has no unbounded path: actingUserId is required, so a key is always somebody, and it faces exactly the floors that somebody faces. A key that names nobody does not skip the guard — it gets a 400 workspace/missing-user-id.string
New cosmetic title (
edit-member-profile, or self for own title). Nullable.object
New cosmetic metadata (
edit-member-profile).string
required
Service/master keys only — the acting user (sent in the body; the path
:userId is the target). Required for a key: this route has no unbounded path, so a key that names nobody is refused with 400 workspace/missing-user-id. Every check below — the two capability tiers, the rank guard, the assign rule and no-escalation — runs against the named user. Act as the owner for an unrestricted edit.A plain user token omits it — the actor is the token’s own user. The field is not ignored for such a token: sending your own user id is a harmless no-op, but sending anyone else’s is a 403 workspace/unauthorized. See Acting on behalf of a user.Response
Returns the updated WorkspaceMember object.Error Responses
Unauthorized — 403
Unauthorized — 403
One message per tier, depending on which fields the request touched. Powerful fields (Cosmetic fields (
capabilities, permissions, rank, relativeRank):{ "error": "The 'edit-member-access' capability is required to edit access fields.", "code": "workspace/unauthorized" }
title, metadata) — not raised when a member is editing only their own title. Editing your own metadata still requires edit-member-profile:{ "error": "The 'edit-member-profile' capability is required to edit profile fields.", "code": "workspace/unauthorized" }
Insufficient Rank — 403
Insufficient Rank — 403
Two distinct messages share this code. The act rule, on the target:…and the assign rule, on the rank you asked for — checked against the resolved absolute value, so
{ "error": "You may only manage members ranked strictly below you.", "code": "workspace/insufficient-rank" }
rank and relativeRank face it identically. This is also what refuses rank: 0 to anyone holding a row on this workspace:{ "error": "You may only set ranks strictly below your own.", "code": "workspace/insufficient-rank" }
Rank out of range — 400
Rank out of range — 400
{ "error": "The resolved rank (2147483648) is out of range — ranks are integers from 0 to 2147483647.", "code": "workspace/invalid-body" }
rank / relativeRank input is refused earlier by the schema, with the same code.Both rank coordinates supplied — 400
Both rank coordinates supplied — 400
{ "error": "relativeRank: Provide either rank (absolute) or relativeRank (offset from you), not both", "code": "workspace/invalid-body" }
No Escalation — 403
No Escalation — 403
The offending values are named. Capabilities:…and permissions:Only added values are checked — removing a capability or permission is governed by rank alone.
{ "error": "Cannot grant capabilities you do not hold on this workspace: edit-member-access.", "code": "workspace/no-escalation" }
{ "error": "Cannot grant permissions you do not hold on this workspace: deploy.", "code": "workspace/no-escalation" }
Missing Acting User — 400
Missing Acting User — 400
{ "error": "Missing user ID", "code": "workspace/missing-user-id" }
actingUserId. Every gate on this route is measured against the editor’s own standing, so the edit must be performed as somebody — act as the owner for an unrestricted edit.Acted as Another User — 403
Acted as Another User — 403
{ "error": "Unauthorized", "code": "workspace/unauthorized" }
actingUserId naming a different user. Only a service/master key may act as someone else.Invalid Path Parameter — 400
Invalid Path Parameter — 400
{ "error": "Invalid workspace id: expected a UUID.", "code": "workspace/invalid-params" }
400 rather than a 500 from the database.
