Skip to content

Import/Export

View .md

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.

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
Terminal window
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

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
Terminal window
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

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.

Terminal window
curl -H "Authorization: Bearer $COMVI_API_KEY" "https://api.comvi.io/v1/translations?locales=en,uk"

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
Terminal window
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/validate

That 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
Terminal window
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" }

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
Terminal window
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]
}

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