Import/Export
Comvi has two import and export flows. The web flow validates files, lets a person review conflicts, then commits. The CLI flow takes a plain translation object and commits it in two calls. Both read and write JSON and i18next v4.
Every endpoint accepts a session cookie or an API key, except the three CLI endpoints, which reject a session and require Authorization: Bearer. A key needs translations:read to export and translations:write to import. Common errors: 401 AUTHENTICATION_REQUIRED, 403 FORBIDDEN (missing project.export or project.import, or a key from another project), 403 READ_ONLY_MODE on imports. See Errors.
POST /v1/projects/{projectId}/keys/export
Section titled “POST /v1/projects/{projectId}/keys/export”Export translations as a downloadable file. Omitting localeCodes exports every locale; omitting namespaces exports every namespace, not only default. Language restrictions narrow both. A completed export fires the export.completed webhook; a download cut off before the stream ends sends nothing.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
keyIds |
body | integer[] |
No | Only these keys |
localeCodes |
body | string[] |
No | Default all locales |
namespaces |
body | string[] |
No | Default all namespaces |
format |
body | string |
No | json or i18next-v4, default json |
statuses |
body | string[] |
No | translated, not_reviewed, not_translated |
includeEmpty |
body | boolean |
No | Include keys with no value, default false |
structure |
body | string |
No | flat or nested, default flat |
style |
body | string |
No | pretty or minified, default pretty |
The response carries Content-Disposition: attachment; use curl -o. The file depends on how many locale and namespace pairs the request resolves to:
| Request resolves to | Content-Type | Filename |
|---|---|---|
| A full export of one locale and one namespace | application/json |
<locale>.json |
| A full export of anything else | application/zip |
<project name>-<YYYY-MM-DD>.zip, with characters outside A-Za-z0-9-_ in the name replaced by _ |
A keyIds batch export for one locale |
application/json |
<locale>.json. Namespaces are flattened into it, and keys outside default are prefixed <namespace>. |
A keyIds batch export for two or more locales |
application/zip |
translations-selected-<YYYY-MM-DD>.zip |
curl -X POST -H "Authorization: Bearer $COMVI_API_KEY" -H "Content-Type: application/json" \ -d '{"localeCodes": ["en"], "namespaces": ["default"], "format": "json", "structure": "flat"}' \ -o en.json https://api.comvi.io/v1/projects/1/keys/export{ "welcome.message": "Welcome to our app!", "welcome.subtitle": "Get started in seconds" }| Status and code | When |
|---|---|
404 PROJECT_NOT_FOUND |
No such project |
422 VALIDATION_ERROR |
format, structure or style is not a listed value. meta.field names it, no message |
GET /v1/projects/{projectId}/export
Section titled “GET /v1/projects/{projectId}/export”CLI export: one JSON body grouped by namespace and locale. API key only; a session gets 401 UNAUTHORIZED with the message API key authentication required, and a key for another project gets 403 FORBIDDEN. Omitting locales returns every locale you are allowed to see, never more than your allowed set.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
locales |
query | string |
No | Comma-separated locale codes |
namespaces |
query | string |
No | Comma-separated namespace names |
statuses |
query | string |
No | Comma-separated statuses |
includeEmpty |
query | boolean |
No | Include keys with no value |
curl -H "Authorization: Bearer $COMVI_API_KEY" "https://api.comvi.io/v1/projects/1/export?locales=en,uk&namespaces=default"{ "locales": ["en", "uk"], "namespaces": { "default": { "en": { "welcome.message": "Welcome to our app!" }, "uk": { "welcome.message": "Ласкаво просимо!" } } }}| Status and code | When |
|---|---|
401 UNAUTHORIZED |
A session cookie was used |
GET /v1/translations
Section titled “GET /v1/translations”The same export for the key’s own project, with no project ID in the path. API key only, translations:read. Query and response are those of GET /v1/projects/{projectId}/export.
curl -H "Authorization: Bearer $COMVI_API_KEY" "https://api.comvi.io/v1/translations?locales=en,uk"Web import
Section titled “Web import”Three steps: validate, optionally re-validate after the user edits mappings, then commit. Step 1 returns a validationId that the later steps pass back. The validation session lives in Redis for 1 hour and is bound to the user who created it.
| Limit | Value | When crossed |
|---|---|---|
| File size | 10 MB per file | 413 FILE_TOO_LARGE. options.maxFileSize can lower it, never raise it |
| Request body | 50 MB | Fastify rejects the request before the handler |
| Total keys | 100,000, or options.maxTotalKeys |
200 with the message in errors[], so read that array |
| Files per ZIP | 500 | 400 TOO_MANY_KEYS |
| Files per request | 1000, counting files extracted from ZIPs | Validation stops at the 1000th; 200 with the message in errors[] |
| Uncompressed ZIP size | 200 MB | 413 FILE_TOO_LARGE |
| ZIP compression ratio | 100:1 | 400 ZIP_BOMB_DETECTED |
| Import rate | 100 in a burst, refilling 20 per minute | 429 RATE_LIMIT_EXCEEDED |
| Session lifetime | 1 hour | 401 IMPORT_SESSION_EXPIRED |
POST /v1/projects/{projectId}/keys/import/validate
Section titled “POST /v1/projects/{projectId}/keys/import/validate”Upload files for validation. Comvi detects format, locale and namespace, and lists conflicts with existing translations. Accepted formats are .json and .zip, checked against their magic numbers; a ZIP is expanded and each JSON inside validated on its own.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
files[].filename |
body | string |
Yes | Original filename, used for format detection |
files[].content |
body | string |
Yes | Base64-encoded content. Raw JSON text fails magic-number detection with 400 FILE_TYPE_NOT_ALLOWED |
files[].size |
body | integer |
Yes | Decoded size in bytes, checked against the 10 MB cap before decoding |
options.maxFileSize |
body | integer |
No | Lower the per-file cap; a larger value is ignored |
options.maxTotalKeys |
body | integer |
No | Lower the 100,000 key cap |
options.defaultConflictResolution |
body | string |
No | keep_existing or use_imported |
options.allowNewLocales |
body | boolean |
No | Allow locales not yet in the project |
curl -X POST -H "Authorization: Bearer $COMVI_API_KEY" -H "Content-Type: application/json" \ -d '{"files": [{"filename": "en.json", "content": "ewogICJ3ZWxjb21lLm1lc3NhZ2UiOiAiSGVsbG8hIiwKICAid2VsY29tZS5zdWJ0aXRsZSI6ICJHZXQgc3RhcnRlZCIKfQ==", "size": 70}], "options": {"defaultConflictResolution": "use_imported"}}' \ https://api.comvi.io/v1/projects/1/keys/import/validateThat content decodes to { "welcome.message": "Hello!", "welcome.subtitle": "Get started" }. errors and warnings can be non-empty on a 200; the total-keys cap reports there rather than failing the request.
{ "validationId": "val_abc123", "files": [{ "filename": "en.json", "detection": { "filename": "en.json", "detectedLocale": "en", "detectedNamespace": "default", "confidence": 0.95, "keyCount": 2, "errors": [], "warnings": [] }, "conflicts": [], "isValid": true }], "summary": { "totalFiles": 1, "totalKeys": 2, "uniqueKeys": 2, "totalConflicts": 0, "validFiles": 1, "invalidFiles": 0 }, "errors": [], "warnings": []}POST /v1/projects/{projectId}/keys/import/revalidate
Section titled “POST /v1/projects/{projectId}/keys/import/revalidate”Re-validate after the user changes file-to-locale mappings, to find new conflicts.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
validationId |
body | string |
Yes | From step 1 |
fileMappings[].filename |
body | string |
Yes | Filename |
fileMappings[].locale |
body | string |
Yes | Target locale code |
fileMappings[].namespace |
body | string |
No | Target namespace |
fileMappings[].overrideDetection |
body | boolean |
No | Use the mapping even when detection disagrees |
conflictResolutions |
body | object[] |
No | Resolved conflicts, shape below |
Conflict resolution object, used here and in commit. existingValue and newValue are required even though Comvi knows both; copy them from the conflicts array of step 1.
| Field | Type | Required | Meaning |
|---|---|---|---|
key |
string |
Yes | Translation key |
locale |
string |
Yes | Locale code |
existingValue |
string |
Yes | Value already in the project |
newValue |
string |
Yes | Value in the imported file |
namespace |
string |
No | Default default |
resolution |
string |
No | keep_existing or use_imported. Falls back to options.defaultConflictResolution from step 1 |
{ "conflicts": [{ "filename": "en.json", "key": "welcome.message", "namespace": "default", "locale": "en", "existingValue": "Welcome to our app!", "newValue": "Hello!", "resolution": "use_imported" }], "warnings": [] }POST /v1/projects/{projectId}/keys/import/commit
Section titled “POST /v1/projects/{projectId}/keys/import/commit”Commit the validated import.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
validationId |
body | string |
Yes | From step 1 |
fileMappings |
body | object[] |
Yes | Same shape as revalidate |
conflictResolutions |
body | object[] |
Yes | Same shape as revalidate. Send [] when none |
options.dryRun |
body | boolean |
No | Run without writing |
options.idempotencyKey |
body | string |
No | Replay guard for a retried commit |
options.progressTrackingId |
body | string |
No | Your own ID, 1 to 128 characters, to poll progress with |
curl -X POST -H "Authorization: Bearer $COMVI_API_KEY" -H "Content-Type: application/json" \ -d '{"validationId": "val_abc123", "fileMappings": [{"filename": "en.json", "locale": "en", "namespace": "default"}], "conflictResolutions": []}' \ https://api.comvi.io/v1/projects/1/keys/import/commit{ "success": true, "results": { "added": 10, "updated": 5, "skipped": 0 }, "conflictsSummary": { "total": 2, "keepExisting": 1, "useImported": 1 }, "details": [{ "filename": "en.json", "locale": "en", "namespace": "default", "added": 10, "updated": 5, "skipped": 0, "errors": [] }], "errors": [], "warnings": [], "duration": 1250.5}GET /v1/projects/{projectId}/keys/import/progress/{trackingId}
Section titled “GET /v1/projects/{projectId}/keys/import/progress/{trackingId}”Poll a large import by the options.progressTrackingId sent to commit. status is running, completed or failed. Records live for 2 hours.
{ "status": "running", "totalFiles": 5, "processedFiles": 3, "currentFile": "de.json", "results": { "added": 150, "updated": 30, "skipped": 0 }, "startedAt": "2025-01-16T14:00:00.000Z" }CLI import
Section titled “CLI import”Two calls, validate then commit, with the translations inline as JSON: no Base64 and no session to carry. Both require an API key; a session gets 401 UNAUTHORIZED, and a key for another project gets 403 FORBIDDEN.
POST /v1/projects/{projectId}/import/validate
Section titled “POST /v1/projects/{projectId}/import/validate”The body is namespaces, keyed by namespace, then locale, then key. Nothing is written. A locale or namespace the project lacks shows in warnings with type: "unknown_locale" or "unknown_namespace", and its rows are dropped; "merged_locale" reports two spellings of one locale (pt_BR and pt-BR) merged into one, the later spelling winning on a key present in both.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
namespaces |
body | object |
Yes | Keyed by namespace, locale, key |
curl -X POST -H "Authorization: Bearer $COMVI_API_KEY" -H "Content-Type: application/json" \ -d '{"namespaces": {"default": {"en": {"welcome.message": "Hello!"}, "uk": {"welcome.message": "Привіт!"}}}}' \ https://api.comvi.io/v1/projects/1/import/validate{ "valid": true, "stats": { "totalKeys": 3, "newKeys": 1, "existingKeys": 2, "totalTranslations": 3 }, "conflicts": [{ "key": "welcome.message", "namespace": "default", "locale": "en", "localValue": "Hello!", "serverValue": "Welcome to our app!" }], "warnings": []}POST /v1/projects/{projectId}/import/commit
Section titled “POST /v1/projects/{projectId}/import/commit”Commit the same namespaces object. deleteOrphans permanently removes every key absent from the payload, which matters when pushing a subset of namespaces.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
namespaces |
body | object |
Yes | Keyed by namespace, locale, key |
options |
body | object |
Yes | Import options, fields below |
options.conflictResolution |
body | string |
Yes | keep_local, keep_server or fail |
options.createNamespaces |
body | boolean |
No | Create missing namespaces |
options.deleteOrphans |
body | boolean |
No | Delete keys not in the payload. Needs translations:delete on the key and a creator without language restrictions, else 403 FORBIDDEN |
The response differs from the web commit: stats rather than results, and errors entries are { key, namespace, message }.
{ "success": true, "stats": { "keysCreated": 1, "keysUpdated": 2, "keysDeleted": 0, "translationsCreated": 1, "translationsUpdated": 2, "namespacesCreated": [] }, "errors": [], "affectedKeyIds": [812, 813, 814]}Import errors
Section titled “Import errors”Standard envelope, except 422 validation failures, which carry code and meta and no message. IMPORT_SESSION_EXPIRED is a 401 and IMPORT_SESSION_MISMATCH a 403, not 400.
| Status and code | When |
|---|---|
400 FILE_TYPE_NOT_ALLOWED |
The decoded bytes are neither JSON nor ZIP, including raw text sent instead of Base64 |
400 INVALID_FILE_STRUCTURE |
content is not valid Base64, or the decoded JSON does not parse |
400 TOO_MANY_KEYS |
A ZIP holds more than 500 files |
400 ZIP_BOMB_DETECTED |
A ZIP’s compression ratio is over 100:1 |
401 UNAUTHORIZED |
CLI endpoints: a session cookie was used |
401 IMPORT_SESSION_EXPIRED |
validationId is older than 1 hour. Run step 1 again |
403 IMPORT_SESSION_MISMATCH |
validationId belongs to another project or user |
403 LIMIT_EXCEEDED |
The import would exceed the plan’s translation key limit |
409 TRANSACTION_CONFLICT |
Either commit: the write transaction hit a deadlock. Retry |
413 FILE_TOO_LARGE |
A file is over 10 MB or over options.maxFileSize, or a ZIP expands past 200 MB |
422 FIELD_REQUIRED |
files, validationId, fileMappings, conflictResolutions, namespaces or options is missing |
422 VALIDATION_ERROR |
conflictResolution is not one of the three values, or a field has the wrong type |
429 RATE_LIMIT_EXCEEDED |
Over the token-bucket allowance, which refills at 20 per minute |
Next steps
Section titled “Next steps”- Translations for single keys and values
- CLI for automated pull and push
- Import/Export in the platform for the dashboard flow