# Glossaries

Manage the organization glossary, its terms and translations, and import or export terminology through the Comvi REST API.

Canonical: https://comvi.io/docs/api/glossaries

Each organization has one glossary; its terms are the editable resource. Project-scoped glossary routes under `/v1/projects/{projectId}/glossary/...` accept an API key and are covered in the [platform glossary guide](/docs/platform/glossaries).

Every endpoint needs a signed-in session, and an API key is turned away in one of two places. The term list, the single-term read and the three exports accept either method, so a key authenticates and is then refused with `403 FORBIDDEN` and the message `API keys cannot access organization-scoped routes`, before scopes are looked at. Every other endpoint is session-only and answers a key with `401 AUTHENTICATION_REQUIRED`.

Permissions: `glossary.view` to read and export, `glossary.create` to create and import, `glossary.edit` to update, `glossary.delete` to delete. `glossary.delete` has no entry in the API-key scope map, so no scope grants it anywhere, including the project-scoped routes. The other three map to `translations:read` and `translations:write`.

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

Read the organization glossary, creating the default record when necessary. Session only, `glossary.view`.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `orgId` | path | `integer` | Yes | Organization ID |

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

### `GET /v1/organizations/{orgId}/glossary/terms`

List terms with cursor pagination. `glossary.view`.

| 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 |
| `search` | query | `string` | No | Up to 255 characters |
| `termType` | query | `preferred \| forbidden` | No | Filter by term type |

```bash
curl -b cookies.txt "https://api.comvi.io/v1/organizations/1/glossary/terms?search=dashboard&limit=10"
```

```json
{ "terms": [], "nextCursor": null, "hasMore": false }
```

### `POST /v1/organizations/{orgId}/glossary/terms`

Create a term. Session only, `glossary.create`.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `orgId` | path | `integer` | Yes | Organization ID |
| `term` | body | `string` | Yes | The term |
| `definition`, `termType`, `partOfSpeech`, `gender` | body | `string` | No | Metadata |
| `isCaseSensitive`, `isExactMatch`, `isTranslatable` | body | `boolean` | No | Matching flags |

```bash
curl -X POST -b cookies.txt -H "Content-Type: application/json" \
-d '{"term":"dashboard","definition":"Main analytics screen","termType":"preferred"}' \
https://api.comvi.io/v1/organizations/1/glossary/terms
```

Returns `201 Created` with the complete term, including its translations.

### `GET /v1/organizations/{orgId}/glossary/terms/{termId}`

Read one term. `glossary.view`.

### `PATCH /v1/organizations/{orgId}/glossary/terms/{termId}`

Update any subset of the fields accepted on create. Session only, `glossary.edit`.

### `DELETE /v1/organizations/{orgId}/glossary/terms/{termId}`

Delete a term. Session only, `glossary.delete`. Returns `204 No Content`.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `orgId` | path | `integer` | Yes | Organization ID |
| `termId` | path | `integer` | Yes | Term ID |

```bash
curl -X DELETE -b cookies.txt https://api.comvi.io/v1/organizations/1/glossary/terms/7
```

### `POST /v1/organizations/{orgId}/glossary/terms/{termId}/translations`

Add a translation to a term. Session only, `glossary.edit`.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `orgId` | path | `integer` | Yes | Organization ID |
| `termId` | path | `integer` | Yes | Term ID |
| `localeId` | body | `integer` | Yes | Target locale ID |
| `text` | body | `string` | Yes | Translated term |
| `termType` | body | `string` | No | `preferred` or `forbidden` |

```bash
curl -X POST -b cookies.txt -H "Content-Type: application/json" \
-d '{"localeId": 2, "text": "панель керування", "termType": "preferred"}' \
https://api.comvi.io/v1/organizations/1/glossary/terms/7/translations
```

### `PATCH /v1/organizations/{orgId}/glossary/terms/{termId}/translations/{translationId}`

Update a translation by its ID. Session only, `glossary.edit`.

### `DELETE /v1/organizations/{orgId}/glossary/terms/{termId}/translations/{translationId}`

Remove a translation by its ID. Session only, `glossary.delete`.

### `POST /v1/organizations/{orgId}/glossary/import`

Import terms from CSV. Session only, `glossary.create`. The body is JSON with the CSV Base64-encoded in `content`. The schema caps the encoded string at 10 MiB, but the route sets no body limit of its own, so Fastify's default of 1 MiB caps the request, roughly 780 KB of decoded CSV. A larger body is refused with `413` and `code: "UNKNOWN"`.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `orgId` | path | `integer` | Yes | Organization ID |
| `content` | body | `string` | Yes | Base64-encoded CSV |
| `conflictStrategy` | body | `skip \| overwrite \| merge` | Yes | What to do with an existing term |
| `columnMapping` | body | `object` | No | Maps your headers to Comvi's column names, such as `{"language": "locale_code", "word": "term"}` |

CSV columns, as the export writes them so an exported file re-imports without a mapping: `concept_id`, `definition`, `subject`, `locale_code`, `term`, `term_type`, `part_of_speech`, `is_case_sensitive`, `is_exact_match`, `is_translatable`, `is_forbidden`, `description`, `gender`. `locale_code` and `term` are required. A header named `locale` instead of `locale_code` is not recognised: the import answers `200` with `{"imported": 0, "skipped": 0, "updated": 0, "errors": [{"row": 0, "message": "Missing locale_code column"}]}` and writes nothing, and a missing `term` column behaves the same way. Read `errors`, not the status. When a row omits `concept_id`, each translation row must follow the source-locale row it belongs to; rows are grouped by order.

```bash
curl -X POST -b cookies.txt -H "Content-Type: application/json" \
-d '{"content":"Y29uY2VwdF9pZCxsb2NhbGVfY29kZSx0ZXJtLGRlZmluaXRpb24KMSxlbixkYXNoYm9hcmQsTWFpbiBhbmFseXRpY3Mgc2NyZWVuCjEsdWss0L/QsNC90LXQu9GMINC60LXRgNGD0LLQsNC90L3RjywK","conflictStrategy":"merge"}' \
https://api.comvi.io/v1/organizations/1/glossary/import
```

That `content` decodes to:

```
concept_id,locale_code,term,definition
1,en,dashboard,Main analytics screen
1,uk,панель керування,
```

### `POST /v1/organizations/{orgId}/glossary/import/json`

Import terms from JSON. Session only, `glossary.create`. The body mirrors the JSON export, so a glossary round-trips between organizations. Up to 100,000 concepts; the body is capped at 10 MB.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `orgId` | path | `integer` | Yes | Organization ID |
| `version` | body | `string` | Yes | `"1.0"` |
| `concepts` | body | `object[]` | Yes | Up to 100,000 |
| `concepts[].terms` | body | `object[]` | Yes | Each with `localeCode` and `text`; optional `termType`, `description`, `partOfSpeech`, `gender`, `isCaseSensitive`, `isExactMatch` |
| `concepts[].definition` | body | `string` | No | Up to 2000 characters |
| `concepts[].subject` | body | `string` | No | Up to 255 characters |
| `concepts[].isTranslatable` | body | `boolean` | No | `false` for names never translated |
| `conflictStrategy` | body | `skip \| overwrite \| merge` | Yes | What to do with an existing term |
| `sourceLocaleCode` | body | `string` | No | Source locale of the concepts |

```bash
curl -X POST -b cookies.txt -H "Content-Type: application/json" \
-d '{"version":"1.0","sourceLocaleCode":"en","conflictStrategy":"merge","concepts":[{"definition":"Main analytics screen","isTranslatable":true,"terms":[{"localeCode":"en","text":"dashboard","termType":"preferred"},{"localeCode":"uk","text":"панель керування"}]}]}' \
https://api.comvi.io/v1/organizations/1/glossary/import/json
```

Both imports answer with the same result. A row you cannot write because of a language restriction is reported in `errors` as `Access denied to locale: <code>` and is not counted in `skipped`. Unknown locale codes and rows missing `locale_code` or `term` land there too, with their row number.

```json
{ "imported": 120, "skipped": 4, "updated": 11, "errors": [{ "row": 45, "message": "Unknown locale: xx" }] }
```

### `GET /v1/organizations/{orgId}/glossary/export/csv`

### `GET /v1/organizations/{orgId}/glossary/export/json`

### `GET /v1/organizations/{orgId}/glossary/export/tbx`

Export the glossary as a file with a `Content-Disposition: attachment` header. `glossary.view`. Use `curl -o`.

| Parameter | In | Type | Required | Meaning |
|-----------|----|------|----------|---------|
| `orgId` | path | `integer` | Yes | Organization ID |
| `localeIds` | query | `string` | No | Comma-separated locale IDs |

| Format | Content-Type | Use it for |
|--------|--------------|------------|
| `csv` | `text/csv; charset=utf-8` | Spreadsheets and quick edits |
| `json` | `application/json` | Round-tripping through the JSON import. Body: `{ "version": "1.0", "sourceLocaleCode": "...", "concepts": [...] }` |
| `tbx` | `application/xml; charset=utf-8` | TBX-speaking CAT tools |

```bash
curl -b cookies.txt -o glossary.tbx "https://api.comvi.io/v1/organizations/1/glossary/export/tbx?localeIds=1,2"
```

## Errors

Standard envelope, except `422` validation failures, which carry `code` and `meta` and no `message`.

| Status | When |
|--------|------|
| `400` | Invalid cursor, body, locale, or term shape |
| `401 AUTHENTICATION_REQUIRED` | No session cookie, the session is no longer valid, or an API key on a session-only endpoint |
| `403 FORBIDDEN` | Missing glossary permission or locale access, or an API key on an endpoint that accepts either method |
| `403 READ_ONLY_MODE` | The organization is over its plan limits and can only read |
| `404` | Organization, term, or translation not found |
| `422 FIELD_REQUIRED`, `FIELD_TOO_LONG`, `FIELD_TOO_SHORT`, `VALIDATION_ERROR` | A field is missing, over its length, empty, or has the wrong type, format, pattern, range or enum. `meta.field` names it |

## Next steps

- [Glossaries in the platform](/docs/platform/glossaries) to configure terminology in the dashboard
- [Machine Translation](/docs/api/machine-translation) for glossary-aware suggestions
