Organizations, projects, and members
CRUD surface that v0.1.0 ships so the webapp’s org/project pages have something to call. Every endpoint is @Authenticated; authorization is scoped to organization membership — non-members get NOT_FOUND (404) so the server never discloses whether a private org exists.
Introduced by T118 (orgs UI) + T119 (projects UI + member management) + the matching backend work. All bodies are application/json.
Related: scopes, errors, authentication endpoints.
Organizations
GET /api/v1/organizations — list every org the caller belongs to.
200 OK
{
"data": [
{
"id": "01HT...",
"slug": "acme",
"name": "Acme Corp",
"callerRole": "OWNER",
"createdAt": "2026-04-18T10:45:00Z"
}
]
}
POST /api/v1/organizations — create a new org; the caller is added as OWNER.
{ "name": "Acme Corp", "slug": "acme" }
slugis optional; if omitted, we derive one fromname(lowercase, non-alphanumeric →-, trimmed).- 201 on success with the full body shown above.
- 409
ORG_SLUG_TAKENif the slug is already in use — slugs are unique globally. - 400
VALIDATION_FAILEDifnameis empty / >128 chars, or the derived slug is unusable.
GET /api/v1/organizations/{orgSlug} — single org (ULID or slug both accepted). 404 if you’re not a member.
PATCH /api/v1/organizations/{orgSlug} — rename.
{ "name": "Acme International" }
Returns the updated body. Other fields (billing, BYOK AI config) are not editable in v0.1.0.
Members
GET /api/v1/organizations/{orgSlug}/members — list members. Any member can call.
200 OK
{
"data": [
{
"userId": "01HT...",
"email": "alice@example.com",
"fullName": "Alice Example",
"role": "OWNER",
"invitedAt": "2026-04-18T10:45:00Z",
"joinedAt": "2026-04-18T10:45:00Z"
}
]
}
PATCH /api/v1/organizations/{orgSlug}/members/{userId} — change a member’s role. Caller must be OWNER or ADMIN.
{ "role": "ADMIN" }
- 400
VALIDATION_FAILED(body.role = INVALID) if the role isn’t one ofOWNER/ADMIN/MEMBER. - 409
LAST_OWNERif the change would leave the org with zero OWNERs.
DELETE /api/v1/organizations/{orgSlug}/members/{userId} — remove a member. Idempotent target (404 if they aren’t a member).
- 409
LAST_OWNERif removing the target would leave the org with zero OWNERs.
What about invites?
Explicitly not in v0.1.0. The invite-by-email + pending-acceptance lifecycle needs the token-email plumbing that SSO / SAML / LDAP (Phase 7) brings. Until then, members grow through self-serve org creation — each user makes their own org and runs solo, or an existing member promotes them via the PATCH endpoint once their sub is known.
Projects
GET /api/v1/organizations/{orgSlug}/projects — list every project in the org.
200 OK
{
"data": [
{
"id": "01HT...",
"slug": "marketing",
"name": "Marketing site",
"description": "Website copy",
"baseLanguageTag": "en",
"createdAt": "2026-04-18T10:45:00Z"
}
]
}
POST /api/v1/organizations/{orgSlug}/projects — create.
{
"name": "Marketing site",
"slug": "marketing",
"description": "Website copy",
"baseLanguageTag": "en"
}
slugoptional; derived fromnamewhen absent.descriptionoptional.baseLanguageTagdefaults to"en"when absent.- 409
PROJECT_SLUG_TAKENif the slug collides within the same org (slugs are unique per org, not globally).
GET /api/v1/organizations/{orgSlug}/projects/{projectSlug} — single project.
PATCH /api/v1/organizations/{orgSlug}/projects/{projectSlug} — rename / edit description.
{ "name": "Marketing Site 2.0", "description": null }
Pass null / empty string on description to clear it. The baseLanguageTag is immutable in v0.1.0 (Phase 2 adds a migration path).
Error responses
All endpoints use the uniform error envelope. The codes specific to this surface:
| Code | HTTP | When |
|---|---|---|
NOT_FOUND |
404 | Target org / project / member doesn’t exist, or caller is not a member of the target org |
VALIDATION_FAILED |
400 | Name empty / too long, slug unparseable, role unknown |
ORG_SLUG_TAKEN |
409 | Slug already in use globally |
PROJECT_SLUG_TAKEN |
409 | Slug already in use inside this org |
LAST_OWNER |
409 | Membership change would orphan the org |
UNAUTHENTICATED |
401 | No JWT on the request |
Changelog
First shipped in v0.1.0 (Phase 1 close-out).