Appearance
Team Management Reference
Member and invite endpoints that power the dashboard Team page (organisation-level logins). See the Team Management guide for the operator-facing walkthrough.
Base path: /functions/v1/onehazel-api
Authentication: the /operators/me/* endpoints use a Supabase session JWT (Authorization: Bearer <jwt>) — 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.
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/membersReturns 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/invitesLists 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/invitesCreates 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/resendIssues 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/:idCancels 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/:idChanges 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/:idSuspends a member — they're signed out and blocked from signing back in until reinstated (via PATCH 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 |
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.
Look up an invite (public) {#lookup}
GET /invites/lookup?token=<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/acceptPublic, 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 |