# Team Management Reference Member and invite endpoints that power the dashboard **Team** page (organisation-level logins). See the [Team Management guide](/team-management) for the operator-facing walkthrough. **Base path:** `/functions/v1/onehazel-api` **Authentication:** the `/operators/me/*` endpoints use a **Supabase session JWT** (`Authorization: Bearer `) — they're the same calls the dashboard makes while you're signed in. The acting user's role (`owner` / `admin` / `member`) is resolved from their profile and enforced server-side. The two `/invites/*` endpoints below are **public** and authenticated by the invite token itself. ::: tip Roles - **Owner** — full control; exactly one per org; can't be revoked or demoted here. - **Admin** — can invite, re-role, revoke, and reinstate **Members** only. - **Member** — no management rights. Management endpoints return `403 FORBIDDEN` when the caller isn't an Owner or Admin, and when an Admin tries to act on an Owner or another Admin. ::: --- ## List members {#list-members} ``` GET /operators/me/members ``` Returns the roster of everyone in the caller's organisation. Available to any member of the org. ### Response ```json { "success": true, "data": { "members": [ { "id": "0f8e…", "authUserId": "b2c3…", "email": "owner@acme.com", "displayName": "Alice Owner", "role": "owner", "status": "active", "createdAt": "2026-05-01T09:00:00.000Z", "isSelf": true, "isOwner": true }, { "id": "1a2b…", "authUserId": "c3d4…", "email": "bob@acme.com", "displayName": "Bob Member", "role": "member", "status": "active", "createdAt": "2026-06-10T12:00:00.000Z", "isSelf": false, "isOwner": false } ], "count": 2 } } ``` `status` is `active` or `suspended` (a revoked member). --- ## List pending invites {#list-invites} ``` GET /operators/me/invites ``` Lists pending email invites for the org. **Owner/Admin only.** ### Response ```json { "success": true, "data": { "invites": [ { "id": "inv_db_id", "email": "carol@acme.com", "role": "member", "status": "pending", "expiresAt": "2026-06-25T12:00:00.000Z", "expired": false, "createdAt": "2026-06-18T12:00:00.000Z" } ], "count": 1 } } ``` --- ## Invite a member {#invite} ``` POST /operators/me/invites ``` Creates an invite, emails the recipient a link, and returns the link **once**. **Owner/Admin only.** Admins may only invite the `member` role. ### Request | Field | Type | Required | Description | |---|---|---|---| | `email` | string | Yes | The invitee's email address | | `role` | string | Yes | `admin` or `member` (Admins can only assign `member`) | ### Response (201) ```json { "success": true, "data": { "id": "inv_db_id", "email": "carol@acme.com", "role": "member", "expiresAt": "2026-06-25T12:00:00.000Z", "acceptUrl": "https://app.onehazel.com/accept-invite?token=…", "token": "inv_…" } } ``` The raw `token` and `acceptUrl` are returned **only** on creation — the dashboard surfaces them as a copy-able link in case the email is delayed. Invites expire **7 days** after creation. Re-inviting the same email revokes the prior pending invite first. ### Errors | HTTP | Code | When | |---|---|---| | `400` | `INVALID_INPUT` | Missing/invalid email, or `role` not `admin`/`member` | | `403` | `FORBIDDEN` | Caller isn't Owner/Admin, or an Admin tried to invite an Admin | | `409` | `ALREADY_MEMBER` | That email already has a OneHazel account | --- ## Resend an invite {#resend} ``` POST /operators/me/invites/:id/resend ``` Issues a **fresh** token (invalidating the old link), updates the expiry, and re-emails it. **Owner/Admin only.** Returns the new `acceptUrl` + `token`. ```json { "success": true, "data": { "id": "inv_db_id", "expiresAt": "…", "acceptUrl": "…", "token": "inv_…" } } ``` Returns `404 NOT_FOUND` if there's no pending invite with that id in the org. --- ## Revoke an invite {#revoke-invite} ``` DELETE /operators/me/invites/:id ``` Cancels a pending invite; its link stops working. **Owner/Admin only.** ```json { "success": true, "data": { "id": "inv_db_id", "status": "revoked" } } ``` --- ## Change role / reinstate a member {#patch-member} ``` PATCH /operators/me/members/:id ``` Changes a member's role and/or reinstates a suspended member. **Owner/Admin only**, subject to the role rules (Admins act on Members only; nobody acts on the Owner or themselves). ### Request | Field | Type | Required | Description | |---|---|---|---| | `role` | string | No | New role: `admin` or `member` (Owner only may set `admin`) | | `status` | string | No | Only accepts `"active"` — reinstates a suspended member | At least one of `role` or `status` must be provided. ### Response ```json { "success": true, "data": { "id": "0f8e…", "role": "admin", "status": "active" } } ``` ### Errors | HTTP | Code | When | |---|---|---| | `400` | `INVALID_INPUT` | Acting on yourself, bad `role`, or `status` other than `active` | | `403` | `FORBIDDEN` | Caller can't manage this target (e.g. Admin acting on an Admin/Owner) | | `404` | `NOT_FOUND` | No such member in the org | --- ## Revoke (suspend) a member {#revoke-member} ``` DELETE /operators/me/members/:id ``` Suspends a member — they're signed out and blocked from signing back in until reinstated (via [PATCH](#patch-member) with `status: "active"`). **Owner/Admin only.** ```json { "success": true, "data": { "id": "0f8e…", "status": "suspended" } } ``` ### Errors | HTTP | Code | When | |---|---|---| | `400` | `INVALID_INPUT` | You can't revoke your own access | | `403` | `OWNER_PROTECTED` | The Owner can't be revoked | | `403` | `FORBIDDEN` | Caller can't manage this target | | `404` | `NOT_FOUND` | No such member in the org | ::: warning API keys are not revoked Suspending a member does **not** disable any API keys they created — keys belong to the organisation. Revoke those separately via [`DELETE /api-keys`](/api-reference/api-keys#revoke). ::: --- ## Look up an invite (public) {#lookup} ``` GET /invites/lookup?token= ``` **Public, token-authenticated.** Used by the Accept Invite page to validate a link before showing the form. Always returns `200` with a `valid` flag (it never reveals whether a token "exists"). ```json { "success": true, "data": { "valid": true, "email": "carol@acme.com", "role": "member", "orgName": "Acme" } } ``` When not usable: ```json { "success": true, "data": { "valid": false, "reason": "expired" } } ``` `reason` is one of `invalid`, `expired`, `accepted`, or `revoked`. --- ## Accept an invite (public) {#accept} ``` POST /invites/accept ``` **Public, token-authenticated.** Creates the invitee's login inside the inviting organisation and marks the invite used. After a successful call the dashboard signs the new user in. ### Request | Field | Type | Required | Description | |---|---|---|---| | `token` | string | Yes | The invite token from the link | | `password` | string | Yes | Minimum **8 characters** | | `first_name` | string | Yes | The new user's first name | | `last_name` | string | Yes | The new user's last name | ### Response ```json { "success": true, "data": { /* new member session */ } } ``` ### Errors | HTTP | Code | When | |---|---|---| | `400` | `INVALID_TOKEN` | Token malformed or no longer valid | | `400` | `INVALID_INPUT` | Password under 8 characters | | `409` | `INVITE_USED` | Invite already accepted | | `410` | `INVITE_EXPIRED` | Invite past its 7-day window |