Keys, namespaces, translations
Project-scoped CRUD for the localization data model. Every endpoint is @Authenticated; authorization runs in the service layer via requireProjectAccess, so non-members see NOT_FOUND (404) — the server never discloses whether a private project exists.
Introduced by T208 backend (closes nothing alone; paired with the T207+T208 webapp PR to close #48 + #49). All bodies are application/json.
Related: scopes, errors, organizations-and-projects.
Namespaces
GET /api/v1/organizations/{orgSlug}/projects/{projectSlug}/namespaces — list every namespace in the project.
200 OK
{
"data": [
{ "id": "01HT...", "slug": "web", "name": "Web", "description": null, "createdAt": "2026-04-19T10:00:00Z" }
]
}
POST /api/v1/organizations/{orgSlug}/projects/{projectSlug}/namespaces — create.
{ "name": "Web", "slug": "web", "description": "Web app strings" }
slugoptional; if omitted it’s derived fromname(lowercase, non-alphanumeric →-).- 409
NAMESPACE_SLUG_TAKENif the slug collides inside the same project. - 400
VALIDATION_FAILEDifnameis empty or >128 chars.
Keys
GET /api/v1/organizations/{orgSlug}/projects/{projectSlug}/keys — list.
Query params:
namespace=<slug>— filter by namespace.limit=<int>(default 50, max 200),offset=<int>(default 0).
POST /keys — create.
{
"namespaceSlug": "web",
"keyName": "settings.save",
"description": "Button label on the settings panel"
}
Triggers an Activity(actionType=CREATED) row.
GET /keys/{keyId} — single key with translations + tags + recent activity.
PATCH /keys/{keyId} — rename, change description, change state, move namespace.
{ "keyName": "settings.save.button", "state": "TRANSLATING" }
Writes either UPDATED or STATE_CHANGED Activity depending on which fields changed.
DELETE /keys/{keyId} — soft-delete (sets softDeletedAt). Idempotent. Writes DELETED Activity.
Translations
PUT /api/v1/organizations/{orgSlug}/projects/{projectSlug}/keys/{keyId}/translations/{languageTag} — upsert the translation for a specific language.
{ "value": "Save", "state": "DRAFT" }
languageTagmust match a configuredProjectLanguageon the project.stateis optional; if omitted andvalueis non-empty, state flips toDRAFT. ExplicitAPPROVEDrequires reviewer scope (enforced at the resource).- Writes
TRANSLATEDActivity. - ICU validation of
valueis deferred — wire-up with T203 is a follow-up on this endpoint.
Error codes specific to this surface
| Code | HTTP | When |
|---|---|---|
NOT_FOUND |
404 | Target project / key / namespace doesn’t exist, or caller is not a member |
VALIDATION_FAILED |
400 | Missing required field, bad field length, unknown state enum value |
NAMESPACE_SLUG_TAKEN |
409 | Namespace slug collision inside this project |
KEY_NAME_TAKEN |
409 | (namespace, keyName) collision inside this project |
LANGUAGE_NOT_CONFIGURED |
409 | languageTag on a translation upsert is not in the project’s configured languages |
UNAUTHENTICATED |
401 | No bearer credential on the request |
What’s NOT here yet
- Tag resource. Backend CRUD for tags lands with the webapp PR that needs it.
- Search & filter. Free-text + tag-intersection search is the dedicated FTS path — see T206 / #47 (the architecture page at
docs/architecture/search.mdlands alongside that PR). - Activity timeline endpoint. The Activity rows are written but no
GET /keys/{keyId}/activityis exposed yet; deferred with #46 post-MVP.
Changelog
First shipped in [Unreleased] under T208 backend.