# Workspaces and organizations

> Act in an organization's workspace from the Nodaro API with X-Nodaro-Workspace, bind tokens to a workspace, and manage members, invitations, sharing and usage.

Source: https://nodaro.ai/docs/developers/api/workspaces

**Workspaces** are where an organization's members work together: a class in a school, a team in a company. An API request acts in one workspace when you send its id in the `X-Nodaro-Workspace` header, and the organization endpoints manage everything around it: organizations, members, invitations, join codes, sharing, budgets and usage.

Organizations are a Nodaro Cloud feature and are switched on per instance. Where they are not enabled, the endpoints below do not exist and the header is ignored. Self-hosted builds do not include organizations. Read [Workspaces](https://nodaro.ai/docs/concepts/workspaces) for the concepts.

## Act in a workspace

```http
X-Nodaro-Workspace: 6f1e6b4c-6a4e-4b7b-9d2a-2f0f0a1d9c34
```

The header does two things, and only these two: it decides **which workspace a list returns** and **where a create lands**. It never grants access. Reading, updating, deleting or running something you name by id follows that object's own workspace. A forgotten header cannot hide your work, and a forged one cannot reach anyone else's.

Without the header you work in your personal space, as an account without an organization always does.

**curl**

```bash
curl -s https://app.nodaro.ai/v1/workflows \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "X-Nodaro-Workspace: 6f1e6b4c-6a4e-4b7b-9d2a-2f0f0a1d9c34"
```

**TypeScript SDK**

```ts
// Every request of this client acts in the workspace:
const classClient = createClient({
baseUrl: 'https://app.nodaro.ai',
auth: new StaticTokenAuth(process.env.NODARO_API_KEY!),
workspaceId: '6f1e6b4c-6a4e-4b7b-9d2a-2f0f0a1d9c34',
})

// Or derive a second client from an existing one:
const classroom = client.withWorkspace(workspaceId)
await classroom.workflows.run(workflowId) // lands in the class
await client.workflows.run(workflowId)    // lands in the personal space
```

**CLI**

```bash
nodaro --workspace 6f1e6b4c-6a4e-4b7b-9d2a-2f0f0a1d9c34 workflows list   # this command only
export NODARO_WORKSPACE=6f1e6b4c-6a4e-4b7b-9d2a-2f0f0a1d9c34            # this shell or CI job
nodaro workspace use 6f1e6b4c-6a4e-4b7b-9d2a-2f0f0a1d9c34                # saved on the profile
nodaro workspace current                                                # which one applies, and why
```

`client.withWorkspace` returns a **new** client instead of changing the old one, so two operations running at the same time can never race over which workspace they are in. Pass `null` for the personal space. In the CLI, each of the three ways beats the one below it, and `nodaro workspace use` checks the workspace before saving it.

Send the header only for a workspace you belong to:

| Status | Code | When |
| --- | --- | --- |
| 400 | `validation_error` | The value is not a UUID. |
| 403 | `not_a_member` | You are not a member of that workspace, it does not exist, or its organization is not active. |
| 403 | `member_suspended` | Your membership is suspended. |

Two places never refuse a stale selection, so it can never lock you out: `GET /v1/me` and `GET /v1/workspaces`, and accepting an invitation. There, a workspace you can no longer select is treated as if you sent no header. Those are the endpoints that tell you which workspaces you may select, so clear a cached selection when they stop listing it.

## Bind a token to a workspace

An API token can be bound to one workspace. It then acts as if it sent the header on every request, and an explicit header naming a different workspace answers `400 token_workspace_mismatch`.

```bash
# Bind
curl -X PATCH https://app.nodaro.ai/v1/api-tokens/$TOKEN_ID \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "Content-Type: application/json" \
  -d '{"workspaceId": "6f1e6b4c-6a4e-4b7b-9d2a-2f0f0a1d9c34"}'

# Unbind
curl -X PATCH https://app.nodaro.ai/v1/api-tokens/$TOKEN_ID \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "Content-Type: application/json" \
  -d '{"workspaceId": null}'
```

Token management needs your signed-in session's JWT: an API token or an OAuth token gets `403 forbidden`. You can bind a token only to a workspace you could select with the header, and unbinding is always allowed. Listing your tokens returns the binding as `workspaceId`.

## Where a create lands

- **Inside a workspace, a create lands in the workspace**, never in your personal space. A create that names no project goes to the workspace's project. When the workspace has no project yet, the create answers `409 workspace_has_no_default_project`: name a project and it succeeds.
- **A project you name must belong to the workspace you act in.** A project of another workspace, or of your personal space, answers `404 Project not found`. That is the same answer as a project that does not exist, so the header cannot be used to discover what exists.
- **Creating a project may be limited to admins.** Members then get `403 project_create_not_allowed`.
- **An organization can close the personal space.** Its members then create only inside a workspace, and a create without the header answers `403 personal_space_disabled`. Accounts without an organization are never affected.

## Archived workspaces are read-only

Archiving a workspace keeps everything in it readable and stops new work from being added. Lists behave as before. Every create, whether a project, a workflow, an import or a sub-workflow, answers `409 workspace_archived`, and so does moving work into the workspace. Moving work out stays allowed, because rescuing it is the reason to open an archived workspace. Other writes into it, such as sharing a workflow, answer `403 workspace_archived`. Match on the code, not on the status.

## Who you are: GET /v1/me

`GET /v1/me` returns your profile. On an instance with organizations it also carries `organizations`, `workspaces` and `lastWorkspaceId`, each entry with your own role and status. Organization entries include the resolved `vocabulary`, such as the word the organization uses for a workspace, so a client never hard-codes "Class" or "Team". `GET /v1/workspaces` returns the same workspace list on its own.

Keep the three states of the organization fields apart:

| What you see | What it means | What to do |
| --- | --- | --- |
| The fields are absent | This instance has no organizations. | Never show a workspace switcher. |
| Present and empty | The account belongs to no organization. | Offer to create or join one. |
| `organizationsUnavailable: true` | The lookup failed. | Keep the selection you had. |

## Organization endpoints

Every endpoint needs authentication. Bodies are JSON, responses are `{ "data": … }`, lists are `{ "data": [ … ], "nextCursor": … }`, and ids are UUIDs.

### Organizations

| Method | Path | Who | Body or query |
| --- | --- | --- | --- |
| `POST` | `/v1/orgs` | Anyone signed in | `{ name, kind, slug?, acceptTerms?, settings? }`, where `kind` is `school` or `team`. Returns `201`. |
| `GET` | `/v1/orgs` | Anyone | Your organizations, with your `role` and `memberStatus`. |
| `GET` | `/v1/orgs/:id` | Member | |
| `PATCH` | `/v1/orgs/:id` | Owner, admin | `{ name?, settings? }`. Settings merge key by key, and unknown keys are dropped. |
| `POST` | `/v1/orgs/:id/transfer-ownership` | Owner | `{ userId }`. The new owner must be an active admin, and the old owner becomes an admin in the same step. |
| `DELETE` | `/v1/orgs/:id` | Owner | Refused with `409 has_active_workspaces` while any workspace is not archived. The organization is soft-deleted and its slug freed. |
| `POST` | `/v1/orgs/:id/leave` | Member | The owner cannot leave (`409 owner_cannot_leave`): transfer first. Leaving removes you from every workspace of the organization. |

- **Approval.** A new organization usually starts `pending` and becomes `active` once the platform approves it. Its owner sees it while it waits, nobody else does, and nothing in it can change. Tell whoever created it, or the wait looks like a failure.
- **Schools need terms.** Creating a school requires `acceptTerms: true`, by which the owner confirms the authority to enroll students. Without it, the create answers `400 terms_required`.
- **Slugs** are 1 to 50 lower-case letters, digits and hyphens. A slug you send must be free (`409 name_taken`). Without one, it is derived from the name: "Sunrise School" becomes `sunrise-school`, then `sunrise-school-2`.
- Creating organizations is limited to a few per hour for each user.

### Members

| Method | Path | Who | Body or query |
| --- | --- | --- | --- |
| `GET` | `/v1/orgs/:id/members` | Owner, admin | `?limit=50&cursor=…`, up to 200. Rows: `userId`, `role`, `status`, `joinedAt`, `email`, `displayName`, `avatarUrl`. |
| `PATCH` | `/v1/orgs/:id/members/:userId` | Owner, admin | `{ role?, status? }`: `admin` or `member`, `active` or `suspended`. Not the owner's row. |
| `DELETE` | `/v1/orgs/:id/members/:userId` | Owner, admin | Not the owner. Also removes the person from every workspace of the organization. |

### Workspaces

| Method | Path | Who | Body or query |
| --- | --- | --- | --- |
| `POST` | `/v1/orgs/:id/workspaces` | Owner, admin | `{ name, slug?, description?, settings? }`. Returns `201`. Slugs are unique within the organization. |
| `GET` | `/v1/orgs/:id/workspaces` | Member | Members see their workspaces, owners and admins see all. `?includeArchived=true` adds archived ones. |
| `GET` | `/v1/workspaces` | Anyone | Every workspace you belong to, across organizations. |
| `GET` | `/v1/workspaces/:id` | Workspace member | |
| `PATCH` | `/v1/workspaces/:id` | Workspace admin | `{ name?, description?, settings? }` |
| `POST` | `/v1/workspaces/:id/archive` | Owner, admin | Archive. Reversible, and nothing is destroyed. |
| `POST` | `/v1/workspaces/:id/unarchive` | Owner, admin | Unarchive. |

### Workspace members

| Method | Path | Who | Body or query |
| --- | --- | --- | --- |
| `GET` | `/v1/workspaces/:id/members` | Workspace member | `?limit&cursor`. Members see `userId`, `role`, `displayName`, `avatarUrl` and `addedAt`. Admins also see `status` and `creditCap`. The roster has no email addresses. |
| `POST` | `/v1/workspaces/:id/members` | Workspace admin | `{ userId, role }`, with `role` `admin` or `member`. The person must already be an active member of the organization (`400 not_org_member`): to bring in someone new, invite them. |
| `PATCH` | `/v1/workspaces/:id/members/:userId` | Workspace admin | `{ role?, status?, creditCap? }`. `creditCap` is the member's spending cap, or `null` for none. |
| `DELETE` | `/v1/workspaces/:id/members/:userId` | Workspace admin | Removes the person from the workspace only. They stay in the organization. |

Organization owners and admins are admins of every workspace in the organization without being listed. Adding one of them to a workspace explicitly gives them that explicit role instead, which is how an organization admin can be a plain member of one class.

### Invitations

An invitation names one email address. Its link carries a token that exists only in the email, and the invitee sees what they are invited to before signing in.

| Method | Path | Who | Body or query |
| --- | --- | --- | --- |
| `POST` | `/v1/orgs/:id/invitations` | Owner, admin, and workspace admins into their own workspace when the organization allows it | `{ emails, orgRole?, workspaceId?, workspaceRole? }`. Up to 200 addresses, lower-cased and de-duplicated. Returns `201` with one row per address: `{ email, status, link? }`. |
| `GET` | `/v1/orgs/:id/invitations` | The same people | `?status=` (`open`, `accepted`, `revoked` or `expired`), `workspaceId`, `limit` and `cursor`. Never returns a token. |
| `DELETE` | `/v1/invitations/:id` | The same people | Revoke. The link stops working. Refused on an accepted invitation. |
| `POST` | `/v1/invitations/:id/resend` | The same people | Issues a new token and expiry and sends it again. The previous link stops working. |
| `GET` | `/v1/invitations/by-token/:token` | Public | What the invitee needs to decide: `orgName`, `kind`, `vocabulary`, `inviterName`, `workspaceName`, the email address masked, `expiresAt` and `state`. Limited per IP address. |
| `POST` | `/v1/invitations/:token/accept` | The signed-in invitee | The account's email must match the invitation (`400 email_mismatch`). A second acceptance is refused. |

- **Each address gets its own row.** `status` is `sent`, `link_only` or `failed`, and `link` is present whenever the address was not emailed. An install without a mail provider returns every address as `link_only`. Show those links: the invitation exists either way, and without the link nobody can reach it.
- **Invitations expire after 14 days**, and an organization may send 500 a day (`429 bulk_invite_cap_exceeded`).
- **Inviting an address again** moves its open invitation to a fresh token instead of creating a second one.
- **Inviting an existing member** uses up the invitation but does not change their role or lift a suspension.

### Join codes

A join code is eight characters, short enough to read aloud in a room. It admits a plain **member** into one workspace, never lifts a suspension, and respects the organization's allowed email domains.

| Method | Path | Who | Body or query |
| --- | --- | --- | --- |
| `GET` | `/v1/workspaces/:id/join-code` | Workspace admin | `{ code, enabled, rotatedAt, rotatedBy }`, or `null` when none was created. |
| `POST` | `/v1/workspaces/:id/join-code` | Workspace admin | `{ action }`: `rotate`, `enable` or `disable`. Enabling a workspace that never had a code creates one. Rotating replaces it, and the old code stops working at once. |
| `POST` | `/v1/workspaces/join` | Anyone signed in | `{ code }`. Spoken forms work: `BCDF-GHJK`, lower case, and commonly misheard letters such as `O` for zero. Limited to 10 attempts per minute for each account and 30 for each IP address. |

### Sharing one workflow

A grant gives one person access to one workflow. It can only add access, never remove it.

| Method | Path | Who | Body or query |
| --- | --- | --- | --- |
| `GET` | `/v1/workflows/:id/collaborators` | Anyone who may view the workflow | Rows of `{ userId, name, avatarUrl, role, createdAt }`, never an email address. |
| `POST` | `/v1/workflows/:id/collaborators` | See below | `{ userId }` or `{ email }`, exactly one, plus `role`: `viewer` or `editor`. Returns `201`. Limited to 20 additions per minute. |
| `PATCH` | `/v1/workflows/:id/collaborators/:userId` | Same as `POST` | `{ role }` |
| `DELETE` | `/v1/workflows/:id/collaborators/:userId` | Same as `POST`, or yourself | Anyone may remove their own access. |
| `GET` | `/v1/workflows/shared-with-me` | Anyone signed in | Workflows you hold a grant on, outside your own workspaces, each with its `grantedRole`. Newest first, up to 200. |
| `GET` | `/v1/workflows/:id/access` | Anyone who may view it | `{ access, workspaceId, visibility, canChangeVisibility, canShare, canRun }`, never the graph. |

- **Who may share:** the creator; a workspace admin where admins may edit members' work; and anyone who may edit the workflow where the workspace lets collaborators invite.
- **Adding by email** matches any account's address, whatever its organization. `404` means there is no account, and nothing more is disclosed.
- **Someone outside the workspace is capped at viewing**, even with an editor grant. Running needs active membership, so an outside collaborator can never spend the workspace's credits.
- You cannot grant to the workflow's creator or to yourself (`400`). A grant works at once, with no acceptance step.
- `canChangeVisibility`, `canShare` and `canRun` are three separate rules. Never infer one from another.

A workflow you cannot reach at all answers `404`, the same as one that does not exist. Only when you can already see it and need more does the answer become `403`. Running a workflow that belongs to a workspace needs edit access and active membership.

### Audit log

| Method | Path | Who | Body or query |
| --- | --- | --- | --- |
| `GET` | `/v1/orgs/:id/audit` | Owner, admin | `?cursor&limit`, newest first. Each entry: `{ id, workspaceId, action, targetType, targetId, details, createdAt, actor }`. `actor` is `null` for actions the system took. |

The audit log stays readable while the organization is suspended. `action` is an open list that grows with the product: show the actions you know, and fall back to the raw value for the rest.

## Roles and settings

| Scope | Role | Can |
| --- | --- | --- |
| Organization | `owner` | Everything an admin can, plus transfer ownership and delete the organization. There is exactly one. The owner cannot be suspended, removed or demoted. |
| Organization | `admin` | Manage settings, members other than the owner, and workspaces. Admin of every workspace. |
| Organization | `member` | Belong to the organization and work in the workspaces they are added to. |
| Workspace | `admin` | Manage the workspace's settings and members. |
| Workspace | `member` | Work in the workspace. |

Settings are layered: a workspace's setting wins over the organization's, which wins over the default for the organization's kind. A key that is not set falls through, and `false` is a real value.

| Key | Values | Controls |
| --- | --- | --- |
| `admin_access` | `view` or `edit` | What admins may do with a member's workflow. |
| `default_workflow_visibility` | `private` or `workspace` | The visibility of a new workflow. |
| `member_access_to_shared` | `view` or `edit` | What members may do with a workflow shared with the workspace. |
| `members_can_create_projects` | boolean | Whether members may create projects. |
| `member_caps_enabled` | boolean | Whether per-member credit caps apply. |
| `personal_space_enabled` | boolean | Whether members keep a personal space. |
| `workspace_admins_can_invite` | boolean | Whether workspace admins may invite new people into the organization. |
| `collaborators_can_invite` | boolean | Whether an editor collaborator may invite more collaborators. |

The organization alone also has `allowed_email_domains`, such as `["school.example"]`, and `vocabulary_overrides`, such as `{ "workspace": "Cohort" }`. Both are replaced whole when updated.

A **school** starts with: admins edit, workflows private, members view shared work, members cannot create projects, caps on, personal space on, workspace admins can invite, collaborators cannot. A **team** starts with: admins view, workflows visible to the workspace, members edit shared work, members can create projects, caps off, personal space on, and both invite settings on.

## Budgets and usage

An organization pays for its members' work in three steps, each narrower than the last:

1. **The pool.** The organization buys prepaid credit packs into one pool for the whole organization, through its own checkout.
2. **Allocations.** The owner moves credits from the pool into a workspace's budget. The pool goes down by exactly what the workspace gains. Credits can go back to the pool, but only down to what the workspace has already reserved or spent.
3. **Member caps.** Inside a workspace, an admin can cap what one member may spend. Caps apply only where the workspace's settings turn them on, and never to an organization admin working in the workspace.

Work done inside a workspace is paid by the workspace, and the member's own credits are not touched. Headroom at every level is allocated minus reserved minus spent, and a run that would exceed it is refused with `402 budget_exceeded` or `402 member_cap_exceeded` before it starts.

| Route | Who | Does |
| --- | --- | --- |
| `GET /v1/orgs/:id/credits` | Owner, org admin | The pool, the lifetime purchases, and each workspace's allocation with its reserved and spent credits. |
| `POST /v1/orgs/:id/credits/checkout` | Owner | A checkout URL for one prepaid pack. Body `{ packId }`. |
| `POST /v1/orgs/:id/workspaces/:wsId/allocate` | Owner | Move credits between the pool and a workspace. Body `{ delta }`. Returns the new headroom. |
| `GET /v1/workspaces/:id/budget` | Member | Your spend, cap and headroom. Admins also get a row for each member. |
| `GET /v1/orgs/:id/usage` | Owner, org admin | A usage report for the organization. |
| `GET /v1/workspaces/:id/usage` | Member, workspace admin | The same for one workspace. |

Refunds and disputes take credits back from the organization's pool in proportion, never below zero. Deleting an organization requires taking back every workspace allocation first.

**Automations spend the workspace's credits.** A [Webhook Trigger](https://nodaro.ai/docs/nodes/automate/webhook-trigger) URL is a bearer credential: whoever holds it can start a run, and a run of a workflow in a workspace is paid by that workspace. Every automated run, from a webhook, a schedule or Telegram, checks first that the trigger's creator may still run the workflow. When they no longer can, the automation stops and the run history shows one failed entry with the code `run_requires_authenticated_member`.

### Usage reports

The two usage routes answer who spent how much on what, over a date range:

| Query | Meaning |
| --- | --- |
| `from`, `to` | Inclusive dates, `YYYY-MM-DD`. The range is at most 366 days and defaults to the last 30. |
| `tz` | An IANA time zone for the day buckets. The default is `UTC`. |
| `groupBy` | `workspace` (organization only), `member`, `model`, `day`, or `none` for the individual runs, newest first, paged with `cursor`. |
| `workspaceId`, `userId` | Narrow the report. |
| `format` | `csv` returns the report as CSV. |

- Each row has three credit figures. `credits` is what the runs cost so far: the settled amount for finished runs and the held reservation for runs in flight. `settledCredits` and `inFlightCredits` split that sum.
- A metered run that cost more than the workspace had left is charged up to the headroom, and the platform absorbs the rest. The totals report it as `platformAbsorbedCredits`, and `settledCredits` minus `platformAbsorbedCredits` is what reached the budget. An approved-app markup the budget could not cover is absorbed too and reported apart as `appMarkupAbsorbedCredits`.
- A report with more than 5,000 groups returns `truncated: true`. Only the grouped rows are cut: the totals cover the whole range.
- A plain member sees only their own runs, and grouping by member is refused for them. A workspace admin sees everyone and may filter by `userId`. The organization report is for owners and organization admins.
- CSV exports are UTF-8, RFC 4180, with CRLF line endings and no byte-order mark. A cell that starts with `=`, `+`, `-` or `@` is quoted with a leading apostrophe so a spreadsheet cannot run it. Exports are limited to 10 per minute for each user and are recorded in the audit log.
- When a member deletes their account, their run history goes with it, and the report shows a gap. Their workflows and projects stay with the workspace.

An instance without usage reports answers `404`, and `503 billing_unavailable` while reporting is not yet available.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `validation_error` | A field is invalid, a cursor is malformed, the change is not allowed for that row, or a usage report has a bad date, time zone, range or grouping. |
| 400 | `terms_required` | Creating a school without `acceptTerms: true`. |
| 400 | `not_org_member` | Adding someone to a workspace who is not an active member of its organization. |
| 400 | `token_workspace_mismatch` | A token bound to one workspace, with a header naming another. |
| 400 | `join_code_invalid` | No such code, the code is disabled, or its workspace is archived. One answer for all three. |
| 400 | `invitation_expired`, `invitation_revoked`, `invitation_accepted` | The invitation is past its 14 days, revoked, or already accepted. |
| 400 | `email_mismatch` | The signed-in account's email is not the invited one. |
| 401 | `unauthorized` | No valid credentials. |
| 403 | `insufficient_role` | You are a member, but your role is too low for the action. |
| 403 | `member_suspended` | Your membership is suspended. |
| 403 | `org_not_active` | The organization is pending or suspended, and the action changes something. |
| 403 | `workspace_archived` | A write into an archived workspace. |
| 403 | `not_a_member` | The header names a workspace you cannot select. |
| 403 | `domain_not_allowed` | The organization admits only listed email domains. |
| 404 | `not_found` | No such organization, workspace or member, or one you are not a member of. |
| 404 | `invitation_not_found` | No invitation for that token, including one whose organization is not active. |
| 409 | `name_taken` | The slug you sent is in use. |
| 409 | `already_a_member` | The person is already in the workspace. |
| 409 | `owner_cannot_leave` | Transfer ownership before leaving. |
| 409 | `has_active_workspaces` | Archive every workspace before deleting the organization. |
| 429 | `rate_limit_exceeded` | Too many organizations created, join attempts, or CSV exports. |
| 429 | `bulk_invite_cap_exceeded` | The organization reached its daily invitation limit. |
| 503 | `billing_unavailable` | Usage reporting is not available on this instance yet. |
| 503 | `audit_unavailable` | A CSV export could not be recorded in the audit log, so it was refused. Try again. |

## From the SDK, the CLI and MCP

- **SDK:** `client.organizations` and `client.workspaces` cover these endpoints, including `usage`, `usageRows` and `usageCsv` for reports. `client.me()` carries the organization fields.
- **CLI:** `nodaro org` and `nodaro workspace`, for example `nodaro org invite <orgId> --email ada@school.example --workspace <id>` and `nodaro org usage <orgId> --from 2026-09-01 --to 2026-09-30 --group-by member --csv > september.csv`. The invite command prints one line per address, with the link for any address that was not emailed.
- **MCP:** the `list_workspaces` and `select_workspace` tools. The selection is remembered across sessions and checked again at each one. An OAuth app that uses these tools needs the `workspaces:read` and `workspaces:write` scopes.

Read [The CLI](https://nodaro.ai/docs/developers/cli) and [The SDK](https://nodaro.ai/docs/developers/sdk) for the full command and method lists.

## Frequently asked questions

### How do I make a Nodaro API request act in a workspace?

Send the workspace id in the X-Nodaro-Workspace header. It decides which workspace a list reads and where a create lands. In the SDK use createClient({ workspaceId }) or client.withWorkspace(id), and in the CLI use --workspace.

### Can a wrong workspace header hide my work or reach someone else's?

No. The header decides scope, never access. Reading, changing, deleting or running something by id follows that object's own workspace, and a workspace you do not belong to answers 403 not_a_member.

### Can I tie an API token to one workspace?

Yes. Bind it with PATCH /v1/api-tokens/:id and a workspaceId, from a signed-in session. The token then acts as if it sent the header on every request, and a header naming another workspace answers 400 token_workspace_mismatch.

### Why does a create answer 403 personal_space_disabled?

Your organization requires its members to create work inside a workspace. Send the X-Nodaro-Workspace header with one of your workspaces and the same call succeeds.

### Are organizations available on self-hosted installs?

No. Organizations are a Nodaro Cloud feature, switched on per instance. Self-hosted builds do not include them and ignore the workspace header.

### Why did an invitation come back with a link instead of being emailed?

The install has no mail provider, or the delivery failed. The invitation exists either way, so send the returned link to the person yourself.
