# Teams API

Teams, members, invites and instance settings. 19 operations under /api/v1. Errors are RFC 9457 problem+json with a stable `code` field. The complete schemas are in https://nebulactrl.com/api/v1/openapi.json.

## GET /instance

Instance settings

Whether team creation is allowed; SMTP details only for the instance admin (the first user).

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Response 200: OK (InstanceSettings)
  - `allowTeamCreation` (boolean, required): Whether any user may create a team (the instance admin always may)
  - `canManage` (boolean, required): The caller is the instance admin and may change these settings
  - `smtpConfigured` (boolean, required): Invites can be emailed through the instance relay
  - `smtpFrom` (string)
  - `smtpHost` (string)
  - `smtpInsecureSkipVerify` (boolean)
  - `smtpPasswordSet` (boolean)
  - `smtpPort` (integer (int64))
  - `smtpTls` (enum: implicit | starttls | none)
  - `smtpUsername` (string)
Errors: 401 Unauthorized; 500 Internal Server Error

## PATCH /instance

Change the instance settings

Instance admin only: allow team creation and the SMTP relay used to mail invites when a team has no email channel.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Request body (application/json, required): UpdateInstanceInputBody
  - `allowTeamCreation` (boolean)
  - `smtpFrom` (string)
  - `smtpHost` (string)
  - `smtpInsecureSkipVerify` (boolean)
  - `smtpPassword` (string): Write-only. Empty clears it.
  - `smtpPort` (integer (int64))
  - `smtpTls` (enum: implicit | starttls | none)
  - `smtpUsername` (string)

Response 200: OK (InstanceSettings)
  - `allowTeamCreation` (boolean, required): Whether any user may create a team (the instance admin always may)
  - `canManage` (boolean, required): The caller is the instance admin and may change these settings
  - `smtpConfigured` (boolean, required): Invites can be emailed through the instance relay
  - `smtpFrom` (string)
  - `smtpHost` (string)
  - `smtpInsecureSkipVerify` (boolean)
  - `smtpPasswordSet` (boolean)
  - `smtpPort` (integer (int64))
  - `smtpTls` (enum: implicit | starttls | none)
  - `smtpUsername` (string)
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## GET /invites/{token}

Preview an invite (public)

What the invite link offers: team, role, expiry. 404 `invite_invalid`, 410 `invite_expired` or `invite_used` otherwise. Rate limited per IP.

Authentication: none.

Parameters:
- `token` (path, string, required)

Response 200: OK (InvitePreview)
  - `accountExists` (boolean, required)
  - `email` (string): Set when the invite is bound to one email address
  - `expiresAt` (string (date-time), required)
  - `invitedBy` (string): Display name of the person who created the invite
  - `role` (enum: owner | admin | developer | viewer, required)
  - `teamName` (string, required)
  - `teamSlug` (string, required)
Errors: 404 Not Found (`invite_invalid`); 410 Gone (`invite_expired`); 422 Unprocessable Entity; 429 Too Many Requests; 500 Internal Server Error

## POST /invites/{token}/accept

Accept an invite

Signed in: joins the team and makes it the session's current team. Not signed in: send `name` and `password` (and `email` for link invites) to create the account and join in one step; the session is started. 409 `account_exists` when the email already has an account (sign in instead), 403 `invite_email_mismatch` for an invite sent to another address.

Authentication: none.

Parameters:
- `token` (path, string, required)

Request body (application/json): AcceptInviteInputBody
  - `email` (string): Sign-up only
  - `name` (string): Sign-up only
  - `password` (string): Sign-up only: creates the account

Response 200: OK (AcceptedInvite)
  - `createdAccount` (boolean, required): A new account was created through the invite and signed in
  - `team` (Team, required)
Errors: 403 Forbidden (`invite_email_mismatch`); 404 Not Found; 409 Conflict (`account_exists`); 410 Gone; 422 Unprocessable Entity; 429 Too Many Requests; 500 Internal Server Error

## GET /teams

List the caller's teams

Every team the signed-in user belongs to, with their role in each. `current` marks the team the request acts in.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Response 200: OK (ListTeamsOutputBody)
  - `items` (Team[] | null, required)
Errors: 401 Unauthorized; 500 Internal Server Error

## POST /teams

Create a team

The caller becomes its owner. 403 `team_creation_disabled` when the instance admin turned team creation off.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `Idempotency-Key` (header, string): Client-chosen key (<=255 chars). A retry with the same key replays the original response for 24 hours.

Request body (application/json, required): CreateTeamInputBody
  - `name` (string, required)

Response 201: Created (Team)
  - `createdAt` (string (date-time), required)
  - `current` (boolean, required): The team the caller is acting in
  - `id` (string, required)
  - `memberCount` (integer (int64), required)
  - `name` (string, required)
  - `projectIds` (string[] | null): The caller's project restriction; absent = all projects
  - `role` (enum: owner | admin | developer | viewer, required): The caller's role in this team
  - `slug` (string, required)
Errors: 401 Unauthorized; 403 Forbidden (`team_creation_disabled`); 422 Unprocessable Entity; 500 Internal Server Error

## GET /teams/{teamId}

Get a team

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `teamId` (path, string, required)

Response 200: OK (Team)
  - `createdAt` (string (date-time), required)
  - `current` (boolean, required): The team the caller is acting in
  - `id` (string, required)
  - `memberCount` (integer (int64), required)
  - `name` (string, required)
  - `projectIds` (string[] | null): The caller's project restriction; absent = all projects
  - `role` (enum: owner | admin | developer | viewer, required): The caller's role in this team
  - `slug` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## PATCH /teams/{teamId}

Rename a team

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `teamId` (path, string, required)

Request body (application/json, required): RenameTeamInputBody
  - `name` (string, required)

Response 200: OK (Team)
  - `createdAt` (string (date-time), required)
  - `current` (boolean, required): The team the caller is acting in
  - `id` (string, required)
  - `memberCount` (integer (int64), required)
  - `name` (string, required)
  - `projectIds` (string[] | null): The caller's project restriction; absent = all projects
  - `role` (enum: owner | admin | developer | viewer, required): The caller's role in this team
  - `slug` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /teams/{teamId}

Delete a team

Owners only. Requires `confirm=<slug>` and a team without servers (409 `team_has_servers`). Deletes every project, token and setting of the team.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `teamId` (path, string, required)
- `confirm` (query, string): The team's slug

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`team_has_servers`); 422 Unprocessable Entity; 500 Internal Server Error

## GET /teams/{teamId}/invites

List a team's pending invites

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `teamId` (path, string, required)

Response 200: OK (ListInvitesOutputBody)
  - `items` (TeamInvite[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /teams/{teamId}/invites

Invite someone to a team

With `email` the link is mailed (team email channel, else the instance SMTP relay) and works once for that address; without it a link invite is created with an expiry and a use limit. `delivery` tells whether the email went out; the response always carries the link.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `Idempotency-Key` (header, string): Client-chosen key (<=255 chars). A retry with the same key replays the original response for 24 hours.
- `teamId` (path, string, required)

Request body (application/json, required): CreateInviteInputBody
  - `email` (string): Mail the invite to this address (single use). Omit for a link invite.
  - `expiresInDays` (integer (int64)): Default 7
  - `maxUses` (integer (int64)): Link invites: how many people may use it (default 1)
  - `projectIds` (string[] | null): Restrict the new member to these projects (developer and viewer only)
  - `role` (enum: owner | admin | developer | viewer, required)

Response 201: Created (CreatedInvite)
  - `delivery` (enum: sent | link_only | not_configured | failed, required)
  - `emailError` (string)
  - `invite` (TeamInvite, required)
  - `link` (string, required): Where the invitee accepts: an absolute URL when NEBULACTRL_PUBLIC_URL is set, otherwise a path
  - `token` (string, required): The raw invite token. Shown once.
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /teams/{teamId}/invites/{inviteId}

Revoke an invite

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `teamId` (path, string, required)
- `inviteId` (path, string, required)

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## GET /teams/{teamId}/members

List a team's members

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `teamId` (path, string, required)

Response 200: OK (ListMembersOutputBody)
  - `items` (TeamMember[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## PATCH /teams/{teamId}/members/{userId}

Change a member's role or project restriction

Owners and admins; only owners manage owners. 409 `last_owner` when the change would leave the team without an owner.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `teamId` (path, string, required)
- `userId` (path, string, required)

Request body (application/json, required): UpdateMemberInputBody
  - `allProjects` (boolean): true lifts the project restriction
  - `projectIds` (string[]): Restrict the member to these projects (developer and viewer only); an empty list sees no project
  - `role` (enum: owner | admin | developer | viewer)

Response 200: OK (TeamMember)
  - `email` (string, required)
  - `joinedAt` (string (date-time), required)
  - `name` (string, required)
  - `projectIds` (string[] | null): Absent = every project of the team
  - `role` (enum: owner | admin | developer | viewer, required)
  - `userId` (string, required)
  - `you` (boolean, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`last_owner`); 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /teams/{teamId}/members/{userId}

Remove a member (or leave the team)

Owners and admins remove members; any member may remove themselves. 409 `last_owner` for the last owner.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `teamId` (path, string, required)
- `userId` (path, string, required)

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`last_owner`); 422 Unprocessable Entity; 500 Internal Server Error

## POST /teams/{teamId}/members/{userId}/password-reset

Generate a password reset link for a member

Owners and admins (admins cannot reset an owner); signed-in users only. The link works once for 30 minutes, needs no SMTP setup and is shown once. Using it ends every session of the member. 409 `no_password` for members who only sign in through an identity provider.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `teamId` (path, string, required)
- `userId` (path, string, required)

Response 201: Created (PasswordResetLink)
  - `expiresAt` (string (date-time), required)
  - `link` (string, required): Where the member sets a new password: an absolute URL when NEBULACTRL_PUBLIC_URL is set, otherwise a path
  - `token` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`no_password`); 422 Unprocessable Entity; 500 Internal Server Error

## GET /teams/{teamId}/security

A team's two-factor requirement

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `teamId` (path, string, required)

Response 200: OK (TeamSecurityBody)
  - `enrolled` (integer (int64), required): Members that have 2FA enabled
  - `members` (integer (int64), required)
  - `require2fa` (boolean, required): Members must enrol 2FA before acting in this team
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## PUT /teams/{teamId}/security

Require two-factor authentication of every member of the team

Members without 2FA are blocked from acting in the team until they enrol. The caller must have 2FA themselves (409 `enrol_first`).

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `teamId` (path, string, required)

Request body (application/json, required): SetTeamSecurityInputBody
  - `require2fa` (boolean, required)

Response 200: OK (TeamSecurityBody)
  - `enrolled` (integer (int64), required): Members that have 2FA enabled
  - `members` (integer (int64), required)
  - `require2fa` (boolean, required): Members must enrol 2FA before acting in this team
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`enrol_first`); 422 Unprocessable Entity; 500 Internal Server Error

## POST /teams/{teamId}/switch

Make a team the session's current team

Re-checks membership (404 for non-members) and stores the team in the session. Browser sessions only: an API token belongs to one team.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `teamId` (path, string, required)

Response 200: OK (Team)
  - `createdAt` (string (date-time), required)
  - `current` (boolean, required): The team the caller is acting in
  - `id` (string, required)
  - `memberCount` (integer (int64), required)
  - `name` (string, required)
  - `projectIds` (string[] | null): The caller's project restriction; absent = all projects
  - `role` (enum: owner | admin | developer | viewer, required): The caller's role in this team
  - `slug` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error
