22 Organizations
10 operations.
22.0.1 addOrganizationMember
POST /api/organizations/add-member
Adds a person to an organization with a role (admins only).
Why it exists. ‡ Add a person to an organisation by email address with a given role, so a colleague can be granted access to its datasets. Restricted to administrators.
Called by — components/add-member-form.tsx
Reached from — /organizations/<id>
Adds a user to an organization with a specified role. Authorization is a hand-written role gate (CONTRACT-API-001): the caller’s own membership must be admin in this organization, checked before the write — a non-admin gets 403; a non-member also gets 403 today (the check reads the same way for both). The user is looked up by email via the auth admin API (paginated), and adding an already-existing member is refused with 400.
Authentication: CookieAuth, SupabaseAuth
Uses a service-role client (row-level security bypassed).
Returns — message · member
Field names as the handler returns them; the source states no types for them.
Effect — writes (insert) organization_members
Request body
| Field | Type | Required |
|---|---|---|
email |
string | yes |
organizationId |
string | yes |
role |
string — one of viewer, member, admin |
yes |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
Member added successfully. | |
400 |
Email, organization ID, and role are required, or role is invalid, or the user is already a member. | Error |
401 |
Not authenticated. | Error |
403 |
You don’t have permission to add members to this organization. | Error |
404 |
User not found. | Error |
500 |
Server error. | Error |
22.0.2 createOrganization
POST /api/organizations/create
Creates a new organization.
Why it exists. ‡ Create an organisation and make the caller its administrator.
Called by — app/(authenticated)/organizations/create/page.tsx
Reached from — /organizations/create
Creates a new organization for the authenticated user, who becomes its administrator.
Authentication: CookieAuth, SupabaseAuth
Uses a service-role client (row-level security bypassed).
Returns — success · organization
Field names as the handler returns them; the source states no types for them.
Effect — writes (delete, insert) organization_members · organizations
Request body
| Field | Type | Required |
|---|---|---|
name |
string | yes |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
Organization created successfully. | Organization |
400 |
Organization name is required. | Error |
401 |
Not authenticated. | Error |
500 |
Server error. | Error |
22.0.3 createOrganizationForCallingUser
POST /api/organizations/create-for-user
Creates a fresh organization for yourself (self-service only; creating one for another user would need a platform-admin role, which doesn’t exist yet).
Why it exists. † Create an organisation on behalf of a named user, for provisioning by an operator rather than by the user themselves. Always creates a new one rather than reusing an existing.
Self-service only — userId must be the calling user (403 otherwise; the app has no platform-admin role, so no one may create organizations on another user’s behalf). Always creates a new organization and adds the caller as its admin (service-role write).
Authentication: CookieAuth, SupabaseAuth
Uses a service-role client (row-level security bypassed).
Returns — success · organization · isNewOrganization
Field names as the handler returns them; the source states no types for them.
Effect — writes (delete, insert) organization_members · organizations
Request body
| Field | Type | Required |
|---|---|---|
userId |
string | yes |
name |
string | no |
email |
string | no |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
Organization created. | Organization |
400 |
User ID is required. | Error |
401 |
Unauthorized. | Error |
403 |
Forbidden — userId is not the calling user. | Error |
500 |
Server error. | Error |
22.0.4 deleteOrganization
POST /api/organizations/delete
Deletes an organization (admins only).
Why it exists. ‡ Delete an organisation, refusing while it still owns datasets so the deletion cannot orphan them. Restricted to its administrators.
Called by — components/organization-details.tsx
Reached from — /organizations/<id>
Deletes an organization, refusing while it still owns datasets so the deletion cannot orphan them. Restricted to the organization’s administrators; a non-member gets 404 (RLS-scoped), a member who is not an admin gets 403.
Authentication: CookieAuth, SupabaseAuth
Uses a service-role client (row-level security bypassed).
Returns — success · message
Field names as the handler returns them; the source states no types for them.
Effect — writes (delete) datasets · organization_members · organizations
Request body
| Field | Type | Required |
|---|---|---|
organizationId |
string | yes |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
Organization deleted successfully. | |
400 |
Organization ID is required. | Error |
401 |
Not authenticated. | Error |
403 |
Only organization admins can delete organizations. | Error |
404 |
Organization not found. | Error |
409 |
The organization still owns datasets; delete those first. | |
500 |
Server error. | Error |
22.0.5 listOrganizationMembers
GET /api/organizations/get-members
Lists an organization’s members, with their email addresses.
Why it exists. ‡ List an organisation’s members with their email addresses, for the member management screen. Restricted to members of that organisation.
Called by — components/organization-details.tsx
Reached from — /organizations/<id>
Lists an organization’s members with their email addresses, for the member management screen. Restricted to members of that organization: a non-member gets 403, not an empty list — restored 2026-08-25 after the RLS conversion had made this a 200 with an empty list, indistinguishable from an organization that genuinely has no members (docs/issues/20260813_get_members_returns_empty_not_403.md). The member rows are read through the caller’s RLS-scoped client; a service-role client is used ONLY for the auth admin API that maps member user_ids to emails.
Authentication: CookieAuth, SupabaseAuth
Returns — members
Field names as the handler returns them; the source states no types for them.
Effect — reads organization_members · profiles
Parameters
| Name | In | Required | Type | Notes |
|---|---|---|---|---|
organizationId |
query | yes | string |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
The organization’s members, with email addresses. | |
400 |
Organization ID is required. | Error |
401 |
Not authenticated. | Error |
403 |
The caller is not a member of this organization. | Error |
404 |
Organization not found. | Error |
500 |
Failed to fetch members. | Error |
22.0.6 listUserOrganizations
GET /api/organizations/get-user-organizations
Lists the organizations you belong to, and your role in each one.
Why it exists. ‡ List the organisations the caller belongs to and their role in each, so the interface can offer an organisation switcher and hide administrative controls. Cached briefly, since it is called on nearly every page.
Called by — app/datasets/create/page.tsx · modules/cad-viewer/core/hooks/use-user-organizations.ts · providers/OrganizationProvider.tsx
Reached from — /datasets/<slug> · /datasets/create · /datasets/org/<id> · /w/data-explorer/anomalies/map · /w/data-explorer/cad-viewer
Lists the organizations the caller belongs to and their role in each, so the interface can offer an organization switcher and hide administrative controls. Cached briefly (5 minutes, in-memory), since it is called on nearly every page.
Authentication: CookieAuth, SupabaseAuth
Returns — not readable from the source: the handler returns a value assembled at run time rather than a literal.
Effect — reads organization_members · organizations
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
List of organizations, with the caller’s role in each. | Organization |
401 |
Not authenticated. | Error |
500 |
Server error. | Error |
22.0.7 getOrganizationImageTotals
GET /api/organizations/image-totals
Org-wide image totals, deduplicated across campaigns that share images.
Total / GPS-covered / tagged image counts across every campaign in an organization, deduplicated by storage_path so a photo shared by two campaigns (other_occurrences) is counted once, not once per campaign that shares it. Exists because kap_org_progress and the Role-Progress dashboard both summed each campaign’s own totals directly, which double-counts every shared photo in an organization whose campaigns share images by design — confirmed live 2026-09-01 on an organization whose two campaigns share their entire image set, a roughly 2x overstatement (docs/issues/20260901_asset_image_counts_double_count_shared_campaign_images.md is the sibling fix, for image_asset_link rather than images directly). Restricted to members of the organization, same as listOrganizationMembers.
Authentication: CookieAuth, SupabaseAuth
Returns — …totals · datasetCount
Field names as the handler returns them; the source states no types for them.
Effect — reads datasets · images
Parameters
| Name | In | Required | Type | Notes |
|---|---|---|---|---|
organizationId |
query | yes | string |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
Deduplicated image totals for the organization. | |
400 |
Organization ID is required. | Error |
401 |
Not authenticated. | Error |
403 |
The caller is not a member of this organization. | Error |
500 |
Failed to compute image totals. | Error |
22.0.8 removeOrganizationMember
POST /api/organizations/remove-member
Removes a person from an organization (admins only).
Why it exists. ‡ Remove a person from an organisation, ending their access to its datasets. Restricted to administrators.
Called by — components/organization-details.tsx
Reached from — /organizations/<id>
Removes a user from an organization. Authorization is a hand-written role gate (CONTRACT-API-001): the caller’s own membership must be admin in this organization, checked before the write. An admin may not remove themselves (400) — that stays a separate, hand-written check in the route, since it depends on the fetched membership row.
Authentication: CookieAuth, SupabaseAuth
Returns — message
Field names as the handler returns them; the source states no types for them.
Effect — writes (delete) organization_members
Request body
| Field | Type | Required |
|---|---|---|
memberId |
string | yes |
organizationId |
string | yes |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
Member removed successfully. | |
400 |
Member ID and organization ID are required, or the caller tried to remove themselves. | Error |
401 |
Not authenticated. | Error |
403 |
You don’t have permission to remove members from this organization. | Error |
404 |
Member not found in this organization. | Error |
500 |
Server error. | Error |
22.0.9 renameOrganization
POST /api/organizations/rename
Renames an organization (admins only).
Why it exists. ‡ Rename an organisation.
Called by — components/organization-details.tsx
Reached from — /organizations/<id>
Renames an organization. Authorized by the database, not the handler (CONTRACT-API-001): the “Org admins can update their organization” RLS policy (is_org_admin_secure) admits the write, so a non-admin (or non-member) update matches no row. A caller who can see the organization but is not an admin gets 403; a caller who cannot see it at all gets 404 — a missing record is not distinguishable from a forbidden one.
Authentication: CookieAuth, SupabaseAuth
Returns — success · organization
Field names as the handler returns them; the source states no types for them.
Effect — writes (update) organizations
Request body
| Field | Type | Required |
|---|---|---|
organizationId |
string | yes |
newName |
string | yes |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
Organization renamed successfully. | Organization |
400 |
Organization ID and new name are required. | Error |
401 |
Not authenticated. | Error |
403 |
Only admins can rename organizations. | Error |
404 |
Organization not found. | Error |
500 |
Failed to rename organization. | Error |
22.0.10 updateOrganizationMemberRole
POST /api/organizations/update-member-role
Changes an existing member’s role — viewer, member or admin. An administrator can change other people’s roles but not their own, so an organization is never left without one.
Why it exists. ‡ Change what an existing member of an organization is allowed to do, so a role correction does not require removing and re-inviting the person.
Called by — components/organization-details.tsx
Reached from — /organizations/<id>
Changes an existing member’s role. Authorized by the database, not the handler (CONTRACT-API-001): organization_members_update_policy admits an organization’s admins and excludes the caller’s own row, so an admin acts on other people’s membership and never their own — the same rule the delete policy applies, and what stops an organization losing its last administrator. A membership in an organization the caller is not part of returns 404, indistinguishable from a member id that does not exist. A caller who can see the membership but may not change it gets 403 saying which of the two reasons applies.
Authentication: CookieAuth, SupabaseAuth
Returns — member
Field names as the handler returns them; the source states no types for them.
Effect — writes (update) organization_members
Request body
| Field | Type | Required |
|---|---|---|
organizationId |
string | yes |
memberId |
string | yes |
role |
string — one of viewer, member, admin |
yes |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
The updated membership (or the unchanged one, if the role already matched — idempotent). | |
400 |
Organization ID, member ID and role are required, or role is invalid. | Error |
401 |
Unauthorized. | Error |
403 |
Not an administrator here, or the caller’s own membership. | Error |
404 |
No such membership in the caller’s scope. | Error |
500 |
Server error. | Error |