Translations
A translation key belongs to a project and a namespace and holds one value per locale.
Every endpoint accepts a session cookie or an API key, except the /v1/keys/ routes, which take a key only. Reads need translations:read, writes translations:write, deletes translations:delete. The two status endpoints also need translations:review when the new status is translated; every other endpoint writes "status": "translated" with translations:write alone.
Common errors: 401 AUTHENTICATION_REQUIRED, 403 FORBIDDEN (no permission, or no access to the locale), 403 READ_ONLY_MODE on writes, 409 TRANSACTION_CONFLICT on writes (a deadlock, retry), 422 FIELD_REQUIRED, FIELD_TOO_LONG and VALIDATION_ERROR with meta.field and no message. See Errors.
Key object
Section titled “Key object”{ "id": 42, "key": "welcome.message", "namespace": "default", "namespaceId": 1, "description": "Main welcome message on the homepage", "isPlural": false, "characterLimit": null, "translations": { "en": { "id": 101, "value": "Welcome to our app!", "status": "translated", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "createdBy": 1 } }, "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T11:00:00.000Z"}Statuses are translated, not_reviewed and not_translated.
GET /v1/projects/{projectId}/keys
Section titled “GET /v1/projects/{projectId}/keys”List keys with cursor pagination and filters. The envelope is { data, pagination }; keep calling with cursor: nextCursor while pagination.hasMore is true. totalItems is never filled in.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
cursor |
query | string |
No | nextCursor from the previous response |
limit |
query | integer |
No | 1 to 100, default 50 |
search |
query | string |
No | Case-insensitive substring on the key name or any value |
namespaceIds |
query | integer[] |
No | Filter by namespace |
statuses |
query | string[] |
No | Filter by status |
localeCodes |
query | string[] |
No | Filter by locale |
sortBy |
query | string |
No | key, updatedAt, createdAt |
sortOrder |
query | string |
No | asc, desc |
includeContext |
query | boolean |
No | Inline each key’s collected context, in the shape of GET …/keys/{keyId}/context |
curl -H "Authorization: Bearer $COMVI_API_KEY" "https://api.comvi.io/v1/projects/1/keys?limit=20&search=welcome&sortBy=key"{ "data": [{ "id": 42, "key": "welcome.message", "…": "…" }], "pagination": { "nextCursor": "eyJpZCI6NDN9", "hasMore": true } }| Status and code | When |
|---|---|
400 INVALID_INPUT |
cursor is not one this endpoint issued |
422 VALIDATION_ERROR |
limit outside 1 to 100, or sortBy/sortOrder not in its enum |
GET /v1/keys/{namespace}/{key}
Section titled “GET /v1/keys/{namespace}/{key}”Read one key with all its values. The project comes from the API key.
Path: namespace (name) and key (name, URL-encoded), both required.
curl -H "Authorization: Bearer $COMVI_API_KEY" https://api.comvi.io/v1/keys/default/welcome.messageReturns the key object.
| Status and code | When |
|---|---|
401 UNAUTHORIZED |
A session cookie was used. Message: API key authentication required |
403 FORBIDDEN |
The key lacks translations:read |
404 { "message": "…" } |
No key by that name in that namespace. No code |
PUT /v1/keys/
Section titled “PUT /v1/keys/”Create a key, or merge new translations into an existing one. The project comes from the API key. Idempotent: the same body twice leaves the same result. A namespace that does not exist is created when the key holds translations:write, so a push from a fresh checkout needs no setup. Creating needs translations:write; updating an existing key also asserts translation.view, from translations:read, so a key with translations:write alone creates on the first push and gets 403 FORBIDDEN on the second. Give a pushing key both.
Body: the fields of POST /v1/projects/{projectId}/keys.
curl -X PUT -H "Authorization: Bearer $COMVI_API_KEY" -H "Content-Type: application/json" \ -d '{"key": "welcome.message", "namespace": "marketing", "translations": {"en": {"value": "Welcome to our app!", "status": "translated"}}}' \ https://api.comvi.io/v1/keys/Returns 201 Created when created, 200 OK when updated, with the key object.
| Status and code | When |
|---|---|
400 { "message": "…" } |
The namespace could not be created, or the project has no default namespace. No code |
401 UNAUTHORIZED |
A session cookie was used |
403 FORBIDDEN |
Missing translations:write, or translations:read when the key exists |
403 LIMIT_EXCEEDED |
The plan’s key limit. meta carries metric, current, limit |
404 NAMESPACE_NOT_FOUND |
namespaceId names no namespace |
422 FIELD_REQUIRED |
key is missing |
422 FIELD_TOO_LONG |
key, namespace, description or aiContext over its limit |
DELETE /v1/keys/{namespace}/{key}
Section titled “DELETE /v1/keys/{namespace}/{key}”Soft-delete a key by namespace and name. API key with translations:delete; translations:write is not enough.
curl -X DELETE -H "Authorization: Bearer $COMVI_API_KEY" https://api.comvi.io/v1/keys/marketing/welcome.messageReturns 204 No Content.
| Status and code | When |
|---|---|
401 UNAUTHORIZED |
A session cookie was used |
403 FORBIDDEN |
The key lacks translations:delete |
404 { "message": "…" } |
No key by that name in that namespace. No code |
POST /v1/projects/{projectId}/keys
Section titled “POST /v1/projects/{projectId}/keys”Create a key, optionally with initial values.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
key |
body | string |
Yes | 1 to 768 characters |
namespace |
body | string |
No | Namespace name, 1 to 255 characters, default default |
namespaceId |
body | integer |
No | Namespace ID, alternative to namespace |
description |
body | string |
No | Context for translators, up to 5000 characters |
aiContext |
body | string |
No | Context for AI providers, up to 2000 characters |
isPlural |
body | boolean |
No | ICU plural key |
characterLimit |
body | integer |
No | 1 or greater |
translations |
body | object |
No | Per locale code: { "value": string, "status": string }, both required. value up to 65,536 characters |
curl -X POST -H "Authorization: Bearer $COMVI_API_KEY" -H "Content-Type: application/json" \ -d '{"key": "welcome.message", "namespace": "default", "translations": {"en": {"value": "Welcome to our app!", "status": "translated"}}}' \ https://api.comvi.io/v1/projects/1/keysReturns 201 Created with the key object.
| Status and code | When |
|---|---|
400 TRANSLATION_KEY_ALREADY_EXISTS |
The name exists in the namespace. A 409 here is a retryable transaction conflict, so read code before retrying |
403 LIMIT_EXCEEDED |
The plan’s key limit |
422 FIELD_TOO_LONG |
key, namespace, description or aiContext over its limit |
422 VALIDATION_ERROR |
Wrong type, or characterLimit not a positive integer |
PATCH /v1/projects/{projectId}/keys/{keyId}
Section titled “PATCH /v1/projects/{projectId}/keys/{keyId}”Update a key’s metadata. Values go through PUT …/keys/{key}.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
keyId |
path | integer |
Yes | Key ID |
key |
body | string |
No | 1 to 768 characters |
namespaceId |
body | integer |
No | Move to another namespace |
description |
body | string | null |
No | Up to 5000 characters |
aiContext |
body | string | null |
No | Up to 2000 characters |
isPlural |
body | boolean |
No | ICU plural key |
characterLimit |
body | integer | null |
No | 1 or greater |
curl -X PATCH -H "Authorization: Bearer $COMVI_API_KEY" -H "Content-Type: application/json" \ -d '{"description": "Updated welcome message", "characterLimit": 100}' https://api.comvi.io/v1/projects/1/keys/42Returns the updated key object.
| Status and code | When |
|---|---|
400 VALIDATION_ERROR |
namespaceId is in another project, or characterLimit is shorter than an existing value; then meta carries limit, maxExistingLength, keyId |
404 { "message": "Key not found" } |
No such key. No code |
409 VALIDATION_ERROR |
The new name exists in the target namespace |
422 FIELD_TOO_LONG |
key, description or aiContext over its limit |
422 VALIDATION_ERROR |
Wrong type or out of range |
PUT /v1/projects/{projectId}/keys/{key}
Section titled “PUT /v1/projects/{projectId}/keys/{key}”Create or overwrite the value of one key in one locale.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
key |
path | string |
Yes | Key name, URL-encoded |
namespace |
query | string |
No | Default default |
locale |
body | string |
Yes | Locale code |
value |
body | string |
Yes | Translation text, up to 65,536 characters |
status |
body | string |
No | Status |
curl -X PUT -H "Authorization: Bearer $COMVI_API_KEY" -H "Content-Type: application/json" \ -d '{"locale": "uk", "value": "Ласкаво просимо!", "status": "translated"}' \ "https://api.comvi.io/v1/projects/1/keys/welcome.message?namespace=default"{ "id": 102, "value": "Ласкаво просимо!", "status": "translated", "createdAt": "2025-01-15T11:00:00.000Z", "updatedAt": "2025-01-16T09:15:00.000Z", "createdBy": 1 }| Status and code | When |
|---|---|
400 LOCALE_NOT_ENABLED |
locale is not on the project |
400 VALIDATION_ERROR |
value is longer than the key’s characterLimit; meta carries limit and actual |
404 TRANSLATION_KEY_NOT_FOUND |
No such key |
422 FIELD_REQUIRED |
locale or value is missing |
422 VALIDATION_ERROR |
status not in the enum |
DELETE /v1/projects/{projectId}/keys/{keyId}
Section titled “DELETE /v1/projects/{projectId}/keys/{keyId}”Soft-delete a key and all its values.
curl -X DELETE -H "Authorization: Bearer $COMVI_API_KEY" https://api.comvi.io/v1/projects/1/keys/42Returns 204 No Content.
| Status and code | When |
|---|---|
404 { "message": "Translation key not found" } |
No such key. No code |
PUT /v1/projects/{projectId}/translation/{translationId}/status
Section titled “PUT /v1/projects/{projectId}/translation/{translationId}/status”Change the review status of one value. Every status needs translations:write (translation.edit for the locale); translated also needs translations:review. A key with translations:review alone fails the write check first, with 403 FORBIDDEN and meta.requiredPermission set to translation.edit.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
translationId |
path | integer |
Yes | Translation value ID |
status |
body | string |
Yes | New status |
curl -X PUT -H "Authorization: Bearer $COMVI_API_KEY" -H "Content-Type: application/json" \ -d '{"status": "translated"}' https://api.comvi.io/v1/projects/1/translation/102/statusReturns 204 No Content. Read reviewedBy and reviewedAt back from the key list.
| Status and code | When |
|---|---|
404 TRANSLATION_KEY_NOT_FOUND |
No such translation, or it belongs to another project |
422 FIELD_REQUIRED |
status is missing |
422 VALIDATION_ERROR |
status not one of the three values |
GET /v1/projects/{projectId}/keys/ids
Section titled “GET /v1/projects/{projectId}/keys/ids”The IDs of every key matching a filter, without pagination. Query: search, namespaceIds, statuses, localeCodes, all optional, as on the key list.
curl -H "Authorization: Bearer $COMVI_API_KEY" "https://api.comvi.io/v1/projects/1/keys/ids?search=welcome"{ "ids": [42, 43, 87], "count": 3 }GET /v1/projects/{projectId}/keys/{keyId}/context
Section titled “GET /v1/projects/{projectId}/keys/{keyId}/context”The UI context collected for one key: the element it renders in, its role, the space available, and its neighbours. Machine translation reads the same signal. Read-only; context is collected from running apps.
curl -H "Authorization: Bearer $COMVI_API_KEY" https://api.comvi.io/v1/projects/1/keys/42/context{ "observed": true, "hasSignal": true, "context": { "uiType": "button", "translationRole": "action-label", "lengthHint": "short", "neighbors": [{ "role": "sibling", "namespace": "default", "key": "checkout.cancel", "text": "Cancel" }], "screenGroups": ["checkout"], "lastSeenAt": "2026-03-20T09:00:00.000Z", "isStale": false }}observed: false means the key was never seen rendering in a running app. observed: true with hasSignal: false means it was seen but nothing useful resolved. Only when both are true is context present.
| Context field | Type | Meaning |
|---|---|---|
uiType |
string |
Kind of element |
translationRole |
string |
Role in that element |
lengthHint |
string | null |
Room available |
neighbors |
object[] |
Nearby strings, { role, namespace, key, text } |
screenGroups |
string[] |
Screens the key was seen on |
lastSeenAt |
string | null |
Last observation |
isStale |
boolean |
The observation may be outdated. Read it before trusting the rest |
raw |
unknown |
The unresolved profile, present on some responses |
| Status and code | When |
|---|---|
404 { "message": "…" } |
No such key, or another project’s. No code |
GET /v1/projects/{projectId}/keys/tm/search
Section titled “GET /v1/projects/{projectId}/keys/tm/search”Find similar source text already translated in this project. It searches the project’s own values; the organization TMs are searched with POST /v1/projects/{projectId}/tm/search. minSimilarity here is a 0 to 1 fraction, while similarity in the response is a 0 to 100 integer.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
sourceText |
query | string |
Yes | Text to match |
targetLocale |
query | string |
Yes | Locale wanted, 1 to 50 characters |
limit |
query | integer |
No | 1 to 20 |
minSimilarity |
query | number |
No | 0 to 1 |
excludeKeyId |
query | integer |
No | Leave this key out |
excludeKey |
query | string |
No | Leave this key name out |
excludeNamespace |
query | string |
No | Narrows excludeKey to one namespace |
curl -H "Authorization: Bearer $COMVI_API_KEY" "https://api.comvi.io/v1/projects/1/keys/tm/search?sourceText=Welcome%20to%20our%20app&targetLocale=uk&excludeKeyId=42"{ "matches": [{ "keyId": 87, "key": "home.greeting", "sourceText": "Welcome to our app", "targetText": "Ласкаво просимо до нашого застосунку", "similarity": 96 }] }| Status and code | When |
|---|---|
422 FIELD_REQUIRED |
sourceText or targetLocale is missing |
422 VALIDATION_ERROR |
limit or minSimilarity out of range |
GET /v1/projects/{projectId}/keys/events
Section titled “GET /v1/projects/{projectId}/keys/events”A Server-Sent Events stream of changes in a project. Events are filtered by your language restrictions. Open streams are capped per project and per user; over the cap the request fails with 429 RATE_LIMIT_EXCEEDED. A named heartbeat event with a timestamp is written every 15 seconds, so EventSource delivers it and a dropped connection can be detected.
const source = new EventSource('https://api.comvi.io/v1/projects/1/keys/events', { withCredentials: true });source.addEventListener('translation.upserted', (e) => console.log(JSON.parse(e.data)));source.addEventListener('heartbeat', () => { /* reset a watchdog */ });Every payload carries projectId, plus:
| Event | Fires when | Payload |
|---|---|---|
translation.upserted |
A value was written | keyId, translationId, key, locale, namespace, value (may be null), status |
key.created |
A key was created | keyId, key, namespace, isPlural |
key.updated |
Metadata changed | keyId, key, namespace, changes with boolean description, aiContext, characterLimit, isPlural |
key.deleted |
A key was deleted | keyId, key, namespace |
namespace.created |
A namespace was created, including by a push | namespaceId, namespace |
schema.updated |
A key’s generated type surface changed | keyId, key, namespace, params (each { name, type, options?, required }) |
project.locale.updated |
A language was added or removed | locale, action (added or removed). Spelled with dots; the webhook enum uses project.locale_added and project.locale_removed |
batch_mt.scheduled |
Batch MT jobs were queued | sourceLocale, jobs (each { jobId, targetLocale, keyCount }), totalKeys |
batch_mt.started |
A job began | jobId, totalKeys, sourceLocale, targetLocale |
batch_mt.progress |
A job advanced | jobId, processed, total, currentKey |
batch_mt.completed |
A job finished | jobId, processed, failed, failedKeyIds, errors (each { keyId, key, message, type }), duration. type is one of no_source, no_translation, insufficient_credits, provider_error, unknown |
batch_mt.failed |
A job failed outright | jobId, error |
cdn_deploy.started |
A CDN deploy began | nothing further |
cdn_deploy.completed |
A CDN deploy finished | filesUploaded, filesFailed, duration, warningCount, warnings (each { file, key, message }) |
cdn_deploy.failed |
A CDN deploy failed | error |
| Status and code | When |
|---|---|
429 RATE_LIMIT_EXCEEDED |
Too many open streams for the project or user |
An API key gets the same stream at GET /v1/keys/events/stream, for the key’s project and with translations:read: only translation.upserted, key.created, key.updated, key.deleted, namespace.created and schema.updated are sent, in the locales the key’s creator may see, and ?events=key.created,key.deleted (up to 32 names) narrows them. The stream opens with a connected event carrying { projectId, events }, then the same heartbeat events; streams are capped at 50 per project and 10 per key creator.
Batch operations
Section titled “Batch operations”Five endpoints under /v1/projects/{projectId}/keys/batch/ act on many keys at once. They are not atomic: items that succeeded are not rolled back when others fail, so read errors. Each returns { success, processed, failed, duration?, errors: [{ keyId, key, message }] }, duration in milliseconds.
curl -X POST -H "Authorization: Bearer $COMVI_API_KEY" -H "Content-Type: application/json" \ -d '{"keyIds": [42, 43, 44]}' https://api.comvi.io/v1/projects/1/keys/batch/delete{ "success": true, "processed": 3, "failed": 0, "errors": [] }Every body carries keyIds (integer[], required, 1 to 10,000 IDs) plus the fields listed per endpoint.
POST /v1/projects/{projectId}/keys/batch/delete
Section titled “POST /v1/projects/{projectId}/keys/batch/delete”Delete many keys. No further fields.
POST /v1/projects/{projectId}/keys/batch/status
Section titled “POST /v1/projects/{projectId}/keys/batch/status”Set the status of many keys in one locale. Body: localeCode (string, required) and status (string, required). translated also needs translations:review, as on the single-value endpoint.
POST /v1/projects/{projectId}/keys/batch/namespace
Section titled “POST /v1/projects/{projectId}/keys/batch/namespace”Move many keys. Body: targetNamespaceId (integer, required).
POST /v1/projects/{projectId}/keys/batch/copy
Section titled “POST /v1/projects/{projectId}/keys/batch/copy”Copy values from one locale to another. Body: fromLocaleCode (string, required), toLocaleCode (string, required), overwriteExisting (boolean, default false).
POST /v1/projects/{projectId}/keys/batch/clear
Section titled “POST /v1/projects/{projectId}/keys/batch/clear”Remove values in the given locales. Body: localeCodes (string[], required).
| Status and code | When |
|---|---|
422 FIELD_REQUIRED |
keyIds, or the locale field the operation needs, is missing |
422 VALIDATION_ERROR |
keyIds is empty or over 10,000, or a status is not in the enum |
Next steps
Section titled “Next steps”- Import/Export for bulk data
- Machine Translation for suggestions and batch jobs
- Namespaces and Languages for the containers keys live in