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