# Translation Memory

Manage translation memories, their entries and project assignments, search for matches, and import or export TMX through the Comvi REST API.

Canonical: https://comvi.io/docs/api/translation-memory

A translation memory (TM) stores translated segments for reuse across projects. TMs live at the organization level; a project can be assigned several with different priorities, and one of them receives new translations.

The `/v1/organizations/{orgId}/tms` and `/v1/tms/{tmId}/...` endpoints are session-only and need `organization.manage_settings`. An API key is refused before the permission check: the organization route answers `403 FORBIDDEN` with `API keys cannot access organization-scoped routes`, and every `/v1/tms/{tmId}/...` route answers `404` with `Translation memory not found`, so the TM's existence is not disclosed. Three endpoints are project-scoped: `GET …/tm-settings` (`translation_memory.view`) and `POST …/tm/search` (`translation_memory.view` and `translation.view`) accept a key with `translations:read`; `PATCH …/tm-settings` needs `translation_memory.edit`, which no scope grants, so a key gets `403` there.

Common errors: `401 AUTHENTICATION_REQUIRED`, `403 FORBIDDEN`, `403 READ_ONLY_MODE` on writes, `404 NOT_FOUND` on `/v1/tms/{tmId}` routes. The full list is under [Errors](#errors).

## TM object

| Field | Type | Meaning |
|-------|------|---------|
| `id`, `organizationId` | `integer` | IDs |
| `name` | `string` | Name |
| `description` | `string \| null` | Description |
| `sourceLocaleId`, `sourceLocaleCode` | `integer`, `string` | Source language |
| `isDefault` | `boolean` | The organization's default TM |
| `isShared` | `boolean` | Searched by every project with the same source locale |
| `autoPopulate` | `boolean` | New translations are added automatically |
| `onlyApproved` | `boolean` | Only reviewed translations are stored |
| `entriesCount`, `languageCount`, `linkedProjectsCount` | `integer` | Counts |
| `createdAt`, `updatedAt` | `string` | ISO 8601 timestamps |

### `GET /v1/organizations/{orgId}/tms`

List an organization's TMs. A `cursor` this endpoint did not issue is ignored and the first page comes back.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `orgId` | path | `integer` | Yes | Organization ID |
| `cursor` | query | `string` | No | `nextCursor` from the previous page |
| `limit` | query | `integer` | No | 1 to 100, default 50 |

```bash
curl -b cookies.txt https://api.comvi.io/v1/organizations/1/tms
```

```json
{
"tms": [{ "id": 1, "organizationId": 1, "name": "Main TM", "description": null, "sourceLocaleId": 10, "sourceLocaleCode": "en", "isDefault": true, "isShared": true, "autoPopulate": true, "onlyApproved": false, "entriesCount": 12450, "languageCount": 8, "linkedProjectsCount": 3, "createdAt": "2025-01-10T08:00:00Z", "updatedAt": "2025-03-20T14:00:00Z" }],
"nextCursor": null, "hasMore": false
}
```

### `POST /v1/organizations/{orgId}/tms`

Create a TM.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `orgId` | path | `integer` | Yes | Organization ID |
| `name` | body | `string` | Yes | 1 to 255 characters |
| `description` | body | `string` | No | Up to 2000 characters |
| `sourceLocaleId` | body | `integer` | Yes | Source locale ID |
| `isShared` | body | `boolean` | No | Default `false` |
| `onlyApproved` | body | `boolean` | No | Default `false` |

```bash
curl -X POST -b cookies.txt -H "Content-Type: application/json" \
-d '{"name": "Marketing TM", "sourceLocaleId": 10, "isShared": true}' https://api.comvi.io/v1/organizations/1/tms
```

Returns the TM object.

### `GET /v1/tms/{tmId}`

Read one TM. Returns the TM object.

```bash
curl -b cookies.txt https://api.comvi.io/v1/tms/1
```

### `PATCH /v1/tms/{tmId}`

Update settings. Send at least one field; an empty object returns `422 VALIDATION_ERROR` with no `meta`.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `tmId` | path | `integer` | Yes | TM ID |
| `name` | body | `string` | No | New name |
| `description` | body | `string \| null` | No | `null` clears it |
| `isShared`, `autoPopulate`, `onlyApproved` | body | `boolean` | No | Flags |

```bash
curl -X PATCH -b cookies.txt -H "Content-Type: application/json" \
-d '{"autoPopulate": true, "onlyApproved": true}' https://api.comvi.io/v1/tms/2
```

Returns the updated TM object.

### `DELETE /v1/tms/{tmId}`

Permanently delete a TM and all its entries. Export it as TMX first for a backup.

```bash
curl -X DELETE -b cookies.txt https://api.comvi.io/v1/tms/2
```

Returns `204 No Content`.

### `GET /v1/tms/{tmId}/entries`

Page through the segments in a TM.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `tmId` | path | `integer` | Yes | TM ID |
| `cursor` | query | `string` | No | `nextCursor` from the previous page |
| `limit` | query | `integer` | No | 1 to 100, default 50 |
| `targetLocaleId` | query | `integer` | No | Only this target language |
| `sourceProjectId` | query | `integer` | No | Only entries from this project |
| `search` | query | `string` | No | Substring on source or target text, 1 to 500 characters |

```bash
curl -b cookies.txt "https://api.comvi.io/v1/tms/1/entries?targetLocaleId=11&limit=2"
```

```json
{
"entries": [{ "id": 5001, "tmId": 1, "sourceText": "Welcome to the dashboard", "targetText": "Bienvenue sur le tableau de bord", "targetLocaleId": 11, "targetLocaleCode": "fr", "contextKey": "dashboard.welcome", "sourceProjectId": 5, "sourceProjectName": "Web App", "authorId": 42, "authorName": "Jane Smith", "usageCount": 12, "lastUsedAt": "2026-03-20T09:00:00.000Z", "createdAt": "2026-01-15T08:00:00.000Z", "updatedAt": "2026-03-01T11:30:00.000Z" }],
"nextCursor": "NTAwMQ==", "hasMore": true
}
```

### `POST /v1/tms/{tmId}/entries`

Add one segment by hand. This is an upsert: an entry with the same source text, target locale and `contextKey` has its `targetText` overwritten and keeps its ID, and the response is still `201 Created` with `Entry created`. The response carries no entry ID either way.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `tmId` | path | `integer` | Yes | TM ID |
| `sourceText` | body | `string` | Yes | 1 to 10000 characters |
| `targetText` | body | `string` | Yes | 1 to 10000 characters |
| `targetLocaleId` | body | `integer` | Yes | Target locale ID |
| `contextKey` | body | `string` | No | Translation key, for context-aware matching |

```bash
curl -X POST -b cookies.txt -H "Content-Type: application/json" \
-d '{"sourceText": "Welcome to the dashboard", "targetText": "Bienvenue sur le tableau de bord", "targetLocaleId": 11, "contextKey": "dashboard.welcome"}' \
https://api.comvi.io/v1/tms/1/entries
```

```json
{ "message": "Entry created" }
```

### `PUT /v1/tms/{tmId}/entries/{entryId}`

Change an entry's translation. Source text and locale are fixed. `targetText` is the only field and is required; an empty object returns `422 VALIDATION_ERROR` with no `meta`.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `tmId` | path | `integer` | Yes | TM ID |
| `entryId` | path | `integer` | Yes | Entry ID |
| `targetText` | body | `string` | Yes | 1 to 10000 characters |

```bash
curl -X PUT -b cookies.txt -H "Content-Type: application/json" \
-d '{"targetText": "Bienvenue sur votre tableau de bord"}' https://api.comvi.io/v1/tms/1/entries/5001
```

```json
{ "message": "Entry updated" }
```

### `DELETE /v1/tms/{tmId}/entries/{entryId}`

Delete an entry. Returns `204 No Content`.

```bash
curl -X DELETE -b cookies.txt https://api.comvi.io/v1/tms/1/entries/5001
```

### `GET /v1/tms/{tmId}/stats`

Counts for one TM.

```bash
curl -b cookies.txt https://api.comvi.io/v1/tms/1/stats
```

```json
{ "totalEntries": 12840, "uniqueLanguagePairs": 7, "totalUsageCount": 31502 }
```

### `POST /v1/tms/{tmId}/search`

Search one TM for matches, ranked by similarity.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `tmId` | path | `integer` | Yes | TM ID |
| `sourceText` | body | `string` | Yes | 1 to 10000 characters |
| `targetLocaleId` | body | `integer` | Yes | Target locale ID |
| `contextKey` | body | `string` | No | Translation key, up to 512 characters |
| `minSimilarity` | body | `integer` | No | Percentage, 50 to 100, default 50 |
| `maxResults` | body | `integer` | No | 1 to 20, default 5 |

```bash
curl -X POST -b cookies.txt -H "Content-Type: application/json" \
-d '{"sourceText": "Welcome to the dashboard", "targetLocaleId": 11, "minSimilarity": 70}' https://api.comvi.io/v1/tms/1/search
```

```json
{
"matches": [{ "id": 5001, "sourceText": "Welcome to the dashboard", "targetText": "Bienvenue sur le tableau de bord", "targetLocaleId": 11, "similarity": 100, "matchType": "exact", "contextKey": "dashboard.welcome", "sourceProjectName": "Web App", "authorName": "Jane Smith", "tmName": "Main TM", "usageCount": 12 }],
"total": 1
}
```

| `matchType` | Similarity | Meaning |
|-------------|------------|---------|
| `perfect` | 101 | Exact text with identical context key |
| `exact` | 100 | Exact text, different or no context |
| `fuzzy` | 50 to 99 | Partial match by text similarity |

### `POST /v1/tms/{tmId}/find-replace`

Rewrite text across every entry for one target language. Run with `preview: true` first and read `affectedCount`. The `preview` array is present only on a preview; a real run returns `affectedCount` alone.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `tmId` | path | `integer` | Yes | TM ID |
| `targetLocaleId` | body | `integer` | Yes | Only this target language |
| `find` | body | `string` | Yes | 1 to 10000 characters |
| `replace` | body | `string` | Yes | Replacement. Empty string deletes the match |
| `preview` | body | `boolean` | No | Report without writing |

```bash
curl -X POST -b cookies.txt -H "Content-Type: application/json" \
-d '{"targetLocaleId": 11, "find": "tableau de bord", "replace": "panneau", "preview": true}' https://api.comvi.io/v1/tms/1/find-replace
```

```json
{ "affectedCount": 34, "preview": [{ "id": 5001, "originalText": "Bienvenue sur le tableau de bord", "replacedText": "Bienvenue sur le panneau" }] }
```

### `GET /v1/projects/{projectId}/tm-settings`

Which TMs a project searches, ordered by priority descending. Session or API key with `translations:read`.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `projectId` | path | `integer` | Yes | Project ID |

```bash
curl -H "Authorization: Bearer $COMVI_API_KEY" https://api.comvi.io/v1/projects/5/tm-settings
```

```json
{ "assignments": [{ "tmId": 1, "tmName": "Main TM", "priority": 90, "isWritable": true, "penalty": 0 }, { "tmId": 4, "tmName": "Legacy TM", "priority": 20, "isWritable": false, "penalty": 10 }] }
```

| Field | Type | Meaning |
|-------|------|---------|
| `priority` | `integer` | 1 to 100. Higher is searched first |
| `isWritable` | `boolean` | Receives new translations. At most one assignment |
| `penalty` | `integer` | 0 to 100, subtracted from every match's similarity |

Any TM in the organization with `isShared: true` and the project's source locale is searched as well, with priority `0` and penalty `0`, and does not appear here; assign it explicitly to give it a priority or penalty. Assignments whose TM has a different source locale are dropped from the response and the search, and assigning one is refused with `400 INVALID_INPUT`.

### `PATCH /v1/projects/{projectId}/tm-settings`

Replace the whole assignment list; anything left out is unassigned. Session only (`translation_memory.edit`). There is no organization-route guard here, so a key reaches the permission check and gets `403`, not `404`.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `projectId` | path | `integer` | Yes | Project ID |
| `assignments` | body | `object[]` | Yes | Up to 50 |
| `assignments[].tmId` | body | `integer` | Yes | TM ID |
| `assignments[].priority` | body | `integer` | Yes | 1 to 100 |
| `assignments[].isWritable` | body | `boolean` | Yes | At most one `true` |
| `assignments[].penalty` | body | `integer` | Yes | 0 to 100 |

```bash
curl -X PATCH -b cookies.txt -H "Content-Type: application/json" \
-d '{"assignments": [{"tmId": 1, "priority": 90, "isWritable": true, "penalty": 0}, {"tmId": 4, "priority": 20, "isWritable": false, "penalty": 10}]}' \
https://api.comvi.io/v1/projects/5/tm-settings
```

Returns the updated settings in the same shape as the `GET`. Two writable assignments return `400 INVALID_INPUT` with `meta.field` set to `assignments.isWritable`.

### `POST /v1/projects/{projectId}/tm/search`

Search every TM assigned to a project at once, each TM's penalty applied. Session or API key with `translations:read`. The body is the same as `POST /v1/tms/{tmId}/search`; the response is `{ matches, total }` with `tmName` naming the memory. `targetLocaleId` must name an existing locale, or the call fails with `400 INVALID_INPUT` before the permission check.

```bash
curl -X POST -H "Authorization: Bearer $COMVI_API_KEY" -H "Content-Type: application/json" \
-d '{"sourceText": "Welcome to the dashboard", "targetLocaleId": 11, "contextKey": "dashboard.welcome"}' \
https://api.comvi.io/v1/projects/5/tm/search
```

### `POST /v1/tms/{tmId}/import`

Import a TMX file. Capped at 50,000 translation units and 10 MB of XML; split larger files.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `tmId` | path | `integer` | Yes | TM ID |
| `content` | body | `string` | Yes | TMX XML, up to 10 MB |
| `conflictResolution` | body | `string` | No | `skip` (default), `overwrite` or `keep_highest` |
| `localeMapping` | body | `object` | No | File codes to your codes, such as `{"en-US": "en"}` |

```bash
curl -X POST -b cookies.txt -H "Content-Type: application/json" \
-d '{"content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><tmx version=\"1.4\">...</tmx>", "conflictResolution": "overwrite", "localeMapping": {"en-US": "en"}}' \
https://api.comvi.io/v1/tms/1/import
```

```json
{ "imported": 1250, "skipped": 30, "errors": ["Row 45: target locale 'xx' not found"] }
```

`skipped` counts duplicates and invalid segments; `errors` holds a message per failed entry.

### `POST /v1/tms/{tmId}/import/preview`

Parse a TMX file without writing. Read `targetLocales` first: a locale the organization lacks needs a `localeMapping` entry, or its segments are skipped. A file that cannot be parsed returns `400 VALIDATION_ERROR` with the parser's message.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `tmId` | path | `integer` | Yes | TM ID |
| `content` | body | `string` | Yes | TMX XML, up to 10 MB |

```json
{ "sourceLocale": "en", "targetLocales": ["fr", "de"], "totalUnits": 1280, "sampleEntries": [{ "sourceText": "Welcome to the dashboard", "translations": [{ "locale": "fr", "text": "Bienvenue sur le tableau de bord" }] }], "errors": [] }
```

### `GET /v1/tms/{tmId}/export`

Stream the TM as TMX. The response has `Content-Type: application/xml; charset=utf-8` and `Content-Disposition: attachment; filename*=UTF-8''<percent-encoded TM name>.tmx`, with no plain `filename=` parameter.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `tmId` | path | `integer` | Yes | TM ID |
| `targetLocaleId` | query | `integer` | No | Only this target language |

```bash
curl -b cookies.txt -o translations.tmx https://api.comvi.io/v1/tms/1/export
```

## Errors

Standard envelope, except `422` validation failures, which carry `code` and `meta` and no `message`. A `404 NOT_FOUND` on a `/v1/tms/{tmId}` route has the same body for a missing TM, an organization you cannot see, and an API-key request.

| Status and code | When |
|-----------------|------|
| `400 INVALID_INPUT` | A locale ID does not resolve, two assignments are writable, the same `tmId` appears twice, or an assigned TM's source locale differs from the project's |
| `400 VALIDATION_ERROR` | The TMX will not parse, holds more than 50,000 units, or its source locale does not match the memory's |
| `401 AUTHENTICATION_REQUIRED` | No session cookie and no API key |
| `403 FORBIDDEN` | Missing `organization.manage_settings`, or an API key on an organization-scoped route |
| `403 READ_ONLY_MODE` | The organization is over its plan limits and can only read |
| `404 NOT_FOUND` | No such TM, one in an organization you cannot see, or an API-key request |
| `422 FIELD_REQUIRED` | A required field is missing. `meta.field` names it |
| `422 FIELD_TOO_LONG` | `name`, `sourceText`, `targetText` or `find` is over its limit |
| `422 VALIDATION_ERROR` | A value is out of range or not in its enum, including `minSimilarity` outside 50 to 100 and `maxResults` outside 1 to 20 |

## Next steps

- [Translation memory in the platform](/docs/platform/translation-memory) for the dashboard
- [Translations](/docs/api/translations) for the project-level TM search over a project's own values
- [Machine Translation](/docs/api/machine-translation) to combine TM matches with MT
