Workspaces

Every diagram belongs to one workspace. Each person has a personal workspace and can also belong to team workspaces, as an owner, a member or a viewer. Every API key, connected app and dashboard session acts in exactly one workspace and sees only that workspace’s diagrams.

Team workspaces are a beta, open to every account. They are billed from the start: Team costs $8 per paid seat a month or $80 a year, with at least 2 seats, and viewers and agents are free. Team’s features, and the management endpoints below, may change during the beta; the diagram API and MCP tools don’t change with them. GET /v1/auth/me says whether the account can create a team workspace, in features.teams.

One workspace per credential

Roles

OwnerMemberViewer
Find, read, render, history, compare, one diagram’s exportYesYesYes
Create, edit, delete, restoreYesYesNo
Publish, create share linksUnless sharing is offUnless sharing is offNo
Unpublish, list and revoke share links, report a problemYesYesNo
Move a personal diagram inYesYesNo
Export the whole workspaceYesNoNo
Make keys, connect appsWrite or readWrite or readRead only
See and revoke everyone’s keys and connected appsYesNoNo
Invite, invite link, domains, join requestsYesNoNo
Roles, removing people, transfer, rename, sharing switch, deletingYesNoNo
Subscribe, billing portalYesNoNo
Usage and limitsYesYesYes
LeaveUnless the last ownerYesYes

A team workspace can have several owners and always has at least one: removing, demoting or leaving as the last owner answers 409 LAST_OWNER. You are the owner of your personal workspace; the team-only actions answer 400 NOT_A_TEAM_WORKSPACE there.

Agents can’t manage a workspace. Inviting people, roles, domains, the sharing switch, moving a diagram and billing work only from the signed-in dashboard: an API key gets 403 SESSION_REQUIRED, and a connected app’s sign-in works only on /mcp.

Write and read access

Each API key and connected app has write or read access. What it may do is that access narrowed by its person’s role: a viewer’s write key acts read-only, and writes again if they become a member.

Joining a team workspace

An owner adds people in three ways. Each one needs the workspace to be on Team, by a subscription or a plan Zindex provides (409 SUBSCRIPTION_REQUIRED otherwise), and a workspace holds at most 1,000 people (409 WORKSPACE_FULL). Joining needs a signed-in account with a verified email.

The invite link and each domain have a role, viewer (the default) or member, which owners can change at any time. Changing the link’s role keeps the same link.

Domain join

Moving a diagram in

POST /v1/diagrams/{id}/move with { "workspaceId": "..." } moves one diagram from your personal workspace into a team workspace where you are an owner or member. It keeps its ID, every version and its hash chain. A published diagram stays published and its share links keep working, so everyone in the team, and anyone with a link, can still see it.

Public sharing

A team workspace’s owners can turn public sharing off. While it is off, publishing a diagram and creating a share link answer 403 SHARING_DISABLED, over REST and MCP; making a diagram private and revoking links still work.

Seats, billing and limits

A team workspace is on Team or on Free (Limits). Team costs $8 a month or $80 a year for each paid seat.

GET /v1/billing/me returns the selected workspace’s plan, pooled usage, limits and seats (paid, billed, minimum) to anyone in it, and its Stripe state to owners only, including workspace.subscriptionInterval (month or year; null without a subscription, or until Stripe has said) and, for a team workspace, workspace.paidBy (the owner who pays, or null while nobody does) and workspace.paymentMethodNeeded: { since, renewsAt } while it needs a card because its payer stopped being an owner, else null (renewsAt is null until Stripe has said). workspace.complimentary, for anyone in it, is true on a paid plan Zindex provides, with no subscription and no charge for seats.

Leaving, deleting and exporting

Management endpoints

The dashboard calls these with its signed-in session, except the routes that take an emailed or linked token instead (the previews, accepting a link and confirming a deletion). They aren’t in the OpenAPI spec, and an API key gets 403 SESSION_REQUIRED. They take and return JSON. Errors from the routes in these tables are { error, code }, with details for some codes. A 404 NOT_FOUND carries reason: workspace when you aren’t in the workspace (the one in the path, or workspaceId in a join), or it doesn’t exist; otherwise what is gone, member, invitation, invite_link, domain, join_request, key or app.

Workspaces and people

Method and pathWhoDoes
POST /v1/workspacesSigned in, verified emailCreate a team workspace: { name }, 1 to 64 characters. 201 { workspace }; the session switches to it
GET /v1/workspaces/{id}Anyone in it{ workspace }, with diagrams, how many live diagrams it holds (null if it can’t be counted)
PATCH /v1/workspaces/{id}OwnerRename: { name }
POST /v1/workspaces/{id}/selectAnyone in itSwitch the session to it; the next sign-in opens it too
GET /v1/workspaces/{id}/membersAnyone in it{ members, seats }
PATCH /v1/workspaces/{id}/members/{userId}OwnerChange a role: { role }, owner, member or viewer. { member, seats }
DELETE /v1/workspaces/{id}/members/{userId}Owner, or yourselfRemove: { removed: true, keysRevoked, appsRevoked }
POST /v1/workspaces/{id}/leaveAnyone in itLeave, with the same answer
POST /v1/workspaces/{id}/transferOwner{ toUserId, then }: then is member (the default), viewer or leave. { members, seats }
POST /v1/workspaces/{id}/deletion-requestOwner{ confirmedName }, the exact name. 202 { ok, message, expiresAt }, and an email with the link that deletes it
GET /v1/public/workspace-deletions/{token}Anyone with the tokenWhat the confirmation page shows: { workspace: { name }, expiresAt }. Deletes nothing; 410 TOKEN_INVALID once the link no longer works
POST /v1/workspaces/deletion-confirmThe emailed token{ token }. { status: "deleted" }; 410 TOKEN_INVALID when the token is invalid, used or expired, or the owner who asked is no longer one; 502 BILLING_UNAVAILABLE with retryable: true when the subscription can’t be canceled, and nothing is deleted

A workspace is { id, name, kind, role, plan, publicSharingDisabled, createdAt, people: { owners, members, viewers }, seats, subscriptionActive }, where seats is { paid, billed, minimum } for a team workspace and null for a personal one. A member is { userId, name, email, role, joinedVia, joinedAt, invitedBy, you, paysForWorkspace }. invitedBy, and addedBy on a domain, are { userId, name } (the person’s name, else their email), or null once that person has closed their account. GET /v1/auth/me adds workspace (the one the session acts in), workspaces (every one you are in, personal first) and features.teams.

A role change or transfer that adds a seat changes nothing and answers 502 BILLING_UNAVAILABLE when the subscription can’t be updated; try again in a minute.

Method and pathWhoDoes
GET /v1/workspaces/{id}/invitationsOwner{ invitations }, the pending ones
POST /v1/workspaces/{id}/invitationsOwner{ email, role }, role member (the default) or viewer. 201 { invitation }, and an email to the address
DELETE /v1/workspaces/{id}/invitations/{invitationId}OwnerWithdraw it
GET /v1/public/invitations/{token}Anyone with the tokenWhat the invitation page shows: { workspace: { name }, invitedBy: { name }, role, email, expiresAt }, with the address partly hidden
POST /v1/invitations/acceptThe invited address, signed in{ token }. { workspace, role }, with note when you joined as a viewer instead; the session switches to it
GET /v1/workspaces/{id}/invite-linkOwner{ enabled, role, createdAt, createdBy }. role is viewer or member; viewer while there is no link
POST /v1/workspaces/{id}/invite-linkOwnerA new link, with an optional { role }: 201 { url, role, createdAt }, shown once. Without role the link keeps the role it had. The old link stops
PATCH /v1/workspaces/{id}/invite-linkOwner{ role }: change the role, keeping the link. Answers as GET; 404 NOT_FOUND with reason: "invite_link" when there is no link
DELETE /v1/workspaces/{id}/invite-linkOwnerTurn it off: { enabled: false }. Its role goes back to viewer
GET /v1/public/join-links/{token}Anyone with the link{ workspace: { name }, people, role }
POST /v1/join-links/acceptSigned in{ token }. { workspace, role }, with note when you joined as a viewer instead; the session switches to it

Accepting when you are already in the workspace answers 409 ALREADY_MEMBER with workspace.

Domains and join requests

Method and pathWhoDoes
GET /v1/workspaces/{id}/domainsOwner{ domains }, each { domain, joinMode, joinRole, addedBy, createdAt }
POST /v1/workspaces/{id}/domainsOwner{ domain, joinMode, joinRole }, joinMode approval (the default) or auto, joinRole viewer (the default) or member. 201 { domain }
PATCH /v1/workspaces/{id}/domains/{domain}Owner{ joinMode, joinRole }, either or both. { domain }
DELETE /v1/workspaces/{id}/domains/{domain}OwnerStop listing it; pending requests stay for owners to decide
GET /v1/join/availableSigned in{ workspaces, eligibility }: the team workspaces your address’s domain can join, each { id, name, domain, joinMode, joinRole, requested }, and whether you qualify, with reason domain, unverified or google_hd when you don’t. requested is true while your request waits for an owner; with auto you join at once
POST /v1/join/domainSigned in{ workspaceId }. With auto: { joined: true, workspace, role }, with note when you joined as a viewer instead, and the session switches to it. With approval: 202 { requested: true, requestId }
GET /v1/workspaces/{id}/join-requestsOwner{ requests }, the pending ones, each { id, user: { userId, name, email }, domain, joinRole, createdAt }: joinRole is the domain’s, or viewer once it is no longer listed
POST /v1/workspaces/{id}/join-requests/{requestId}/approveOwnerOptional { role }, viewer or member, defaulting to the domain’s joinRole (viewer once the domain is no longer listed). Adds the person: { member }, with note when a member’s seat couldn’t be added and they joined as a viewer. Someone already in the workspace keeps their role
POST /v1/workspaces/{id}/join-requests/{requestId}/declineOwner{ declined: true }; the person can ask again

Everyone’s keys and apps, and sharing

Method and pathWhoDoes
GET /v1/workspaces/{id}/keysOwner{ keys }: every active key in the workspace, whoever made it, with its scope and owner
DELETE /v1/workspaces/{id}/keys/{keyId}OwnerRevoke it: { revoked: true }
GET /v1/workspaces/{id}/appsOwner{ apps }: every connected app that can still act in the workspace
DELETE /v1/workspaces/{id}/apps/{grantId}OwnerDisconnect it: { revoked: true }
GET /v1/workspaces/{id}/sharingAnyone in it{ publicSharingDisabled, publicDiagrams, shareLinks }
PUT /v1/workspaces/{id}/sharingOwner{ publicSharingDisabled }, with the same answer
POST /v1/workspaces/{id}/sharing/make-privateOwner{ unpublished, linksRevoked }

The person whose key or app an owner revokes isn’t told. Everyone manages their own keys with POST /v1/auth/keys and DELETE /v1/auth/keys/{id}, and their own connected apps in the dashboard. The sharing endpoints are for team workspaces only.

Errors

CodeStatusMeans
INVALID_REQUEST400A field is wrong; error says which
NOT_A_TEAM_WORKSPACE400That applies to team workspaces only
PUBLIC_MAIL_DOMAIN400Anyone can get an address at that domain (public mail or a disposable inbox), so it can’t be listed
EMAIL_NOT_ALLOWED400The address can’t have an account and has none; reason is disposable for a disposable inbox
PLAN_NOT_AVAILABLE400Pro is for a personal workspace and Team for a team one
AUTH_REQUIRED401Sign in first
SESSION_REQUIRED403Sent with a key or connected app; use the signed-in dashboard
OWNER_REQUIRED403Only an owner of the workspace can do that
EMAIL_NOT_VERIFIED403Verify your email at https://zindex.ai/verify-email first
INVITATION_EMAIL_MISMATCH403The invitation is for another address; sign in with that one
DOMAIN_NOT_HELD403No owner or member qualifies for the domain yet
NOT_ELIGIBLE403You don’t qualify to join by domain; reason is domain, unverified or google_hd
NOT_FOUND404No such workspace, person, invitation, invite link, domain, request, key or app that you can see; reason says which
LAST_OWNER409The workspace would have no owner; make someone else an owner first
SUBSCRIPTION_REQUIRED409The workspace needs an active subscription before anyone can join
ALREADY_MEMBER409That person is already in the workspace
WORKSPACE_FULL409The workspace has 1,000 people
UNPAID_WORKSPACE_EXISTS409You already created a team workspace that isn’t paid for; workspace names it
JOIN_REQUEST_PENDING409You already asked; an owner will decide
DOMAIN_EXISTS409The workspace already lists that domain
INVITATION_INVALID410The invitation or link expired, was withdrawn or reset, or was used
TOKEN_INVALID410The deletion link is invalid, used or expired, or the owner who asked is no longer one
RATE_LIMITED42950 invitations in 24 hours
BILLING_UNAVAILABLE502The subscription couldn’t be updated, so nothing changed; try again
EMAIL_UNAVAILABLE502The invitation or confirmation email couldn’t be sent, so nothing was sent or requested; try again