Workspace settings
The calls behind Settings in the dashboard: which optional features a workspace uses, the people in it, your own OPVS address, and updates for your machines. The dashboard uses the same routes. For what each settings page does, see Settings: where each setting lives.
Before you call¶
Every route takes a personal access token, Authorization: Bearer $OPVS_PAT, and acts on the workspace the token belongs to. {brand_id} in a path must be that same workspace.
| Group | Who may call | Token scope |
|---|---|---|
| Read feature switches | any active member | brand:read |
| Change a feature switch, members and guests | a workspace admin | brand:admin |
| Your OPVS address | the token's own person | any |
| Machine updates | a workspace admin | any |
brand:read is in every workspace's default scope ceiling. brand:admin is not: a platform admin has to widen your workspace's policy first (see Authentication). Without it, make these changes in the dashboard.
A token that lacks a route's scope gets 403 with a detail that names the missing scope and says whether your workspace allows it.
Feature switches¶
A workspace has four optional features: skills, feed, machines and crm. Turning one off hides it from the dashboard and deletes nothing. See Turn a feature on or off.
GET /api/v1/brands/{brand_id}/features¶
Returns every switch with its current state and what the feature holds.
curl "https://api.opvs.ai/api/v1/brands/1/features" \
-H "Authorization: Bearer $OPVS_PAT"
# Response 200, application/json (two of the four switches shown)
# {"new_navigation": true,
# "switches": {
# "skills": {"on": true, "locked_by_plan": false, "source": "data_default",
# "changed_by": null, "changed_at": null,
# "holds": {"installed_packages": 59}},
# "feed": {"on": true, "locked_by_plan": false, "source": "data_default",
# "changed_by": null, "changed_at": null,
# "holds": {"feed_sources": 8}}}}
# Without a token the route returns a 401 error.
Add ?format=yaml or ?format=md for a smaller answer.
Response
| Field | Type | Meaning |
|---|---|---|
new_navigation |
boolean | Whether the workspace uses the current sidebar and Settings |
switches.<key>.on |
boolean | Whether the feature is on |
switches.<key>.locked_by_plan |
boolean | true when the plan does not include the feature; it cannot be turned on |
switches.<key>.source |
string | Why it has this value: plan_default, data_default (the workspace already had data for it) or admin (an admin set it) |
switches.<key>.changed_by |
object or null | {user_id, name} of the admin who last set it |
switches.<key>.changed_at |
string or null | When an admin last set it |
switches.<key>.holds |
object | Counts of what the feature holds, such as installed_packages, feed_sources, machines or crm_boards |
Errors
| Status | When | Resolution |
|---|---|---|
401 |
no token, or the token is not valid | send Authorization: Bearer $OPVS_PAT with a live token |
403 |
the token lacks brand:read |
request a token with brand:read |
404 Brand not found |
{brand_id} is not a workspace you belong to |
use the token's own workspace id |
422 |
format is not yaml or md |
drop format, or use one of the two values |
PUT /api/v1/brands/{brand_id}/features/{key}¶
Turns one feature on or off for everyone in the workspace, and returns every switch as it now stands. {key} is skills, feed, machines or crm.
curl -X PUT "https://api.opvs.ai/api/v1/brands/1/features/feed" \
-H "Authorization: Bearer $OPVS_PAT" \
-H "Content-Type: application/json" \
-d '{"on": false}'
# Response 200: the same shape as GET /features, with
# switches.feed.on = false, source = "admin" and changed_by naming you.
# A feature your plan does not include returns a 409 error when you turn it on.
Request body: on (boolean, required).
Errors
| Status | When | Resolution |
|---|---|---|
401 |
no token, or the token is not valid | send a live token |
403 |
the token lacks brand:admin |
change the switch in the dashboard, or have your workspace's policy widened to allow brand:admin |
403 Brand admin access required |
you are not an admin of this workspace | ask an admin |
404 Brand not found |
{brand_id} is not a workspace you belong to |
use the token's own workspace id |
409 This feature is not included in the workspace plan |
turning on a feature with locked_by_plan: true |
change plan under Billing & plans first |
422 |
{key} is not one of the four, or on is missing |
send one of the four keys and {"on": true} or {"on": false} |
Members¶
Every route in this group needs brand:admin and an admin of the workspace. {user_id} must be an active member.
PATCH /api/v1/brands/{brand_id}/members/{user_id}¶
Changes a person's role or position in the workspace.
curl -X PATCH "https://api.opvs.ai/api/v1/brands/1/members/482" \
-H "Authorization: Bearer $OPVS_PAT" \
-H "Content-Type: application/json" \
-d '{"role": "admin", "position": "Head of Sales"}'
# Response 200
# {"success": true}
# Making the only admin a member returns a 409 error.
Request body: role (string, optional): admin or member. position (string, optional): free text; send null to clear it. Send at least one.
Errors
| Status | When | Resolution |
|---|---|---|
400 Role must be 'admin' or 'member' |
another role value | send admin or member |
400 No fields to update |
the body has neither field | send role, position or both |
403 |
the token lacks brand:admin, or you are not an admin |
use an admin's brand:admin token |
404 Member not found |
the person is not an active member | check {user_id} against Members |
409 The last workspace admin cannot be changed to a member |
demoting the only admin | make someone else an admin first |
POST /api/v1/brands/{brand_id}/members/{user_id}/photo¶
Uploads a picture for a member. Send the file as multipart field file: PNG, JPG or GIF, up to 5 MB.
curl -X POST "https://api.opvs.ai/api/v1/brands/1/members/482/photo" \
-H "Authorization: Bearer $OPVS_PAT" \
-F "file=@portrait.png"
# Response 200
# {"photo": "https://files.opvs.ai/…/portrait.png"}
# A file over 5 MB returns a 400 error.
Errors
| Status | When | Resolution |
|---|---|---|
400 File must be PNG, JPG or GIF |
another file type | convert the image |
400 File too large (max 5MB) |
the file is over 5 MB | resize it |
403 |
the token lacks brand:admin, or you are not an admin |
use an admin's brand:admin token |
404 Member not found |
the person is not an active member | check {user_id} |
503 Image storage not configured |
picture storage is unavailable | try again later |
DELETE /api/v1/brands/{brand_id}/members/{user_id}/photo¶
Removes a member's picture.
curl -X DELETE "https://api.opvs.ai/api/v1/brands/1/members/482/photo" \
-H "Authorization: Bearer $OPVS_PAT"
# Response 200
# {"photo": null}
# A person who is not an active member returns a 404 error.
Errors
| Status | When | Resolution |
|---|---|---|
403 |
the token lacks brand:admin, or you are not an admin |
use an admin's brand:admin token |
404 Member not found |
the person is not an active member | check {user_id} |
POST /api/v1/brands/{brand_id}/members/{user_id}/reset-password¶
Emails the member a link to choose a new password.
curl -X POST "https://api.opvs.ai/api/v1/brands/1/members/482/reset-password" \
-H "Authorization: Bearer $OPVS_PAT"
# Response 200
# {"success": true, "message": "Password reset email sent to ada@example.com"}
# If the email cannot be sent the route returns a 502 error.
Errors
| Status | When | Resolution |
|---|---|---|
403 |
the token lacks brand:admin, or you are not an admin |
use an admin's brand:admin token |
404 Member not found |
the person is not an active member | check {user_id} |
502 Failed to send password reset email |
the email could not be sent | try again later |
POST /api/v1/brands/{brand_id}/members/{user_id}/set-password¶
Sets a member's password directly. This works only for a person who belongs to this workspace and no other; for everyone else, send a reset link.
curl -X POST "https://api.opvs.ai/api/v1/brands/1/members/482/set-password" \
-H "Authorization: Bearer $OPVS_PAT" \
-H "Content-Type: application/json" \
-d '{"password": "correct-horse-battery", "send_email": true}'
# Response 200
# {"success": true, "message": "Password updated for ada@example.com"}
# A person who belongs to another workspace too returns a 409 error.
Request body: password (string, required, at least 8 characters). send_email (boolean, optional, default false): also email the person that their password changed.
Errors
| Status | When | Resolution |
|---|---|---|
400 User has no auth account |
the person has never signed in with a password | invite them, or send a reset link |
403 |
the token lacks brand:admin, or you are not an admin |
use an admin's brand:admin token |
404 Member not found |
the person is not an active member | check {user_id} |
409 Send a reset link instead of setting this person's password. |
the person is in more than one workspace, or is an OPVS staff or service account | use reset-password |
422 |
password is shorter than 8 characters |
send a longer password |
502 Failed to set password |
the sign-in service did not accept the change | try again later |
Guests¶
A guest is someone outside the workspace who can open some of its boards or docs. Every route here needs brand:admin and an admin of the workspace.
GET /api/v1/brands/{brand_id}/guests¶
Lists every guest and each board or docs space they can open.
curl "https://api.opvs.ai/api/v1/brands/1/guests" \
-H "Authorization: Bearer $OPVS_PAT"
# Response 200
# {"guests": [{"user_id": 913, "email": "sam@example.com", "full_name": "Sam Reyes",
# "image": null,
# "access": [{"kind": "board", "resource_id": "6f1c2a7e-3b0d-4c55-9e1a-2d8f4b7c9a10",
# "name": "Launch plan", "role": "editor"}]}],
# "total": 1}
# Without a token the route returns a 401 error.
Response: guests[].access[].kind is board or docs; role is the guest's level on that board or space, or null.
Errors
| Status | When | Resolution |
|---|---|---|
401 |
no token, or the token is not valid | send a live token |
403 |
the token lacks brand:admin, or you are not an admin |
use an admin's brand:admin token |
502 AgentBoard returned an invalid response |
the boards service answered badly | try again later |
POST /api/v1/brands/{brand_id}/guests/{user_id}/make-member¶
Invites a guest to join the workspace as a member. Their guest access stays until they accept.
curl -X POST "https://api.opvs.ai/api/v1/brands/1/guests/913/make-member" \
-H "Authorization: Bearer $OPVS_PAT"
# Response 201
# {"id": 2207, "email": "sam@example.com", "status": "pending",
# "expires_at": "2026-10-22T16:00:00Z", "created_at": "2026-10-15T16:00:00Z"}
# A guest who already has a pending invitation returns a 400 error.
Errors
| Status | When | Resolution |
|---|---|---|
400 User is already a member of this brand |
the person is already a member | nothing to do |
400 An invitation is already pending for this email |
an invitation is waiting | wait for them to accept; Settings → Members → Invitations lists it |
403 |
the token lacks brand:admin, or you are not an admin |
use an admin's brand:admin token |
404 Guest not found |
the person is not a guest of this workspace | list guests first |
DELETE /api/v1/brands/{brand_id}/guests/{user_id}¶
Removes a guest from every board and docs space in the workspace. Cards, comments and pages they wrote stay.
curl -X DELETE "https://api.opvs.ai/api/v1/brands/1/guests/913" \
-H "Authorization: Bearer $OPVS_PAT"
# Response 200
# {"success": true, "boards_removed": 2, "docs_removed": 1}
# A person who is not a guest returns a 404 error.
Errors
| Status | When | Resolution |
|---|---|---|
403 |
the token lacks brand:admin, or you are not an admin |
use an admin's brand:admin token |
404 Guest not found |
the person is not a guest of this workspace | list guests first |
502 AgentBoard returned an invalid response |
the boards service answered badly | try again later |
Your OPVS address¶
Every person gets an @opvs.run address when they sign up. These routes act on the token's own person.
GET /api/v1/me/mail/address/availability¶
Checks whether a new name for your address is free. name is 3 to 30 lowercase letters, numbers or hyphens, starting and ending with a letter or number.
curl "https://api.opvs.ai/api/v1/me/mail/address/availability?name=ada-lovelace" \
-H "Authorization: Bearer $OPVS_PAT"
# Response 200
# {"name": "ada-lovelace", "domain": "opvs.run", "available": true}
# A name shorter than 3 characters returns a 422 error.
Errors
| Status | When | Resolution |
|---|---|---|
401 |
no token, or the token is not valid | send a live token |
404 Not found |
the token's person has no OPVS address | none to change |
422 |
name breaks the naming rule |
use 3 to 30 lowercase letters, numbers or hyphens |
PUT /api/v1/me/mail/address¶
Changes your address. Mail to the old address is forwarded to the new one for 30 days.
curl -X PUT "https://api.opvs.ai/api/v1/me/mail/address" \
-H "Authorization: Bearer $OPVS_PAT" \
-H "Content-Type: application/json" \
-d '{"name": "ada-lovelace"}'
# Response 200
# {"address": "ada-lovelace@opvs.run",
# "forwarding_from": "ada.l@opvs.run",
# "forwards_until": "2026-11-14T16:00:00Z"}
# A name someone else holds returns a 409 error.
Errors
| Status | When | Resolution |
|---|---|---|
404 Not found |
the token's person has no OPVS address | none to change |
409 This OPVS address is not available |
the name is taken, or is still forwarding for someone else | check availability first and pick another name |
409 This is already your OPVS address |
the name is your current one | nothing to do |
409 This mailbox cannot be renamed |
the address is not on opvs.run |
it cannot be changed |
422 |
name breaks the naming rule |
use 3 to 30 lowercase letters, numbers or hyphens |
Machine updates¶
Ask OPVS to update the software on your machines. A request is queued, never run at once: OPVS updates each machine when no build is running on it. Both routes need an admin of the workspace.
POST /api/v1/machines/{machine_id}/updates¶
Requests an update for one machine.
curl -X POST "https://api.opvs.ai/api/v1/machines/8d2e6b1a-4f3c-4e9a-b7d5-1c0f9a2e3b44/updates" \
-H "Authorization: Bearer $OPVS_PAT"
# Response 202
# {"id": "c1a9e7f0-2b6d-4d38-8e51-7a4b3c2d1e90",
# "machine_id": "8d2e6b1a-4f3c-4e9a-b7d5-1c0f9a2e3b44", "machine_name": "build-01",
# "status": "queued", "requested_by": {"user_id": 482, "name": "Ada Lovelace"},
# "requested_at": "2026-10-15T16:00:00Z", "wait_for_idle": true, "error": null}
# A machine that is not yours returns a 404 error.
Errors
| Status | When | Resolution |
|---|---|---|
403 |
you are not an admin of this workspace | ask an admin |
404 Not found |
no machine with that id in your workspace | check the id under Settings → Machines |
409 This machine shares its sidecar installation with another brand and remains operator-managed. |
the machine is shared with another workspace | OPVS updates it for you; no request is needed |
POST /api/v1/machines/updates¶
Requests an update for every machine in the workspace. Machines that cannot be requested come back with status: "failed" and an error; the rest are queued.
curl -X POST "https://api.opvs.ai/api/v1/machines/updates" \
-H "Authorization: Bearer $OPVS_PAT"
# Response 202 (abridged: one machine, four of its fields)
# {"requests": [{"machine_id": "8d2e6b1a-4f3c-4e9a-b7d5-1c0f9a2e3b44", "machine_name": "build-01",
# "status": "queued", "error": null}],
# "queued": 1, "failed": 0, "requested_at": "2026-10-15T16:00:00Z"}
# Without a token the route returns a 401 error.
Errors
| Status | When | Resolution |
|---|---|---|
401 |
no token, or the token is not valid | send a live token |
403 |
you are not an admin of this workspace | ask an admin |