--- title: "REST API" slug: "monomer-service-rest-api" status: "update" updated: 2026-11-03T16:08:44Z published: 2026-11-03T16:08:44Z canonical: "docs.discngine.com/monomer-service-rest-api" --- > ## Documentation Index > Fetch the complete documentation index at: https://docs.discngine.com/llms.txt > Use this file to discover all available pages before exploring further. # REST API The HTTP interface of the Biotoolkit Monomer Service. It is the interface Ideation and the other Biotoolkit services use, and it is available to your own integrations. ## Base URL and Swagger UI | Item | Value | | --- | --- | | Base path | `https://<host>/HELM2MonomerService/rest` | | Swagger UI | `https://<host>/HELM2MonomerService/` | | OpenAPI document | `https://<host>/HELM2MonomerService/rest/openapi.json` | | Version | `GET /rest/Version` returns the deployed service version as plain text | Ask your Discngine contact for the host of your deployment. The Swagger UI is the authoritative description of the request and response schemas for the version you are running. ## Authentication Every request carries a JWT, presented as `Authorization: Bearer <token>` and issued through the Discngine Admin Center. The token identifies your organisation, and every read and write is scoped to it: you see the public library and your own monomers, and nobody else's. A request without a valid token is answered with `401`. Every write is attributed to the user the token names, in the first of its `username`, `name`, `login` and `email` claims: that is the account the version history and the audit columns show. A token that names no user is attributed to `System`. Public monomers are readable by everyone and writable by nobody: a write against one returns `403`. ## Error envelope Errors carry a JSON body with an `outcome` and a `message`: | `outcome` | Status | Meaning | | --- | --- | --- | | `INVALID_PARAMETER` | 400 | A path or query parameter is not acceptable | | `NOT_FOUND` | 404 | No monomer, rule or image matches | | `VALIDATION_FAILED` | 409 | The monomer violates a rule: duplicate symbol, duplicate structure, duplicate name, missing field, malformed R-groups. A duplicate adds `conflictingSymbol` and `conflictingMonomerId`, naming the existing monomer; a duplicate name also adds `conflictingName`. | | `ALIAS_CONFLICT` | 409 | A name is already taken: an alias equal to a symbol or an alias in your scope, or a new symbol equal to an alias. Adds `conflictingAlias` when the obstacle is an alias, and `conflictingSymbol` and `conflictingMonomerId` naming the monomer that holds the name. | | `ARCHIVED_MATCH` | 409 | The symbol or structure matches a deleted monomer. Adds `matchType` and `archived[]`; revive it, or repeat the `PUT` with `force=true`. | | `MONOMER_UNREADABLE` | 500 | A stored monomer cannot be served, for example because one of its cap groups is not in the vocabulary. Adds `kind` (`CAP_RESOLUTION` or `STRUCTURE_CONVERSION`); the message names the monomer and the reason. Nothing is written by a request that hits it. Report it to Discngine support. | | `INTERNAL_ERROR` | 500 | Unexpected failure | | `SERVICE_MISCONFIGURED` | 503 | The deployment is not configured for the operation. Retrying does not help; report it to Discngine support. | `503` means the database or the chemistry service is unavailable, and carries `Retry-After: 30`. `501` means the operation is not supported by the storage backend of the deployment. ## Monomers `{polymertype}` is `PEPTIDE`, `RNA` or `CHEM`. Read operations also accept `ALL`. | Method | Path | Description | | --- | --- | --- | | `GET` | `/monomer/{polymertype}` | List monomers. Query: `monomertype`, `filter` with `filterField` set to `name` or `symbol` (which also matches aliases), `limit`, `offset`. The search is case-insensitive. Returns `{offset, limit, total, monomers: [...]}`; a monomer appears once, under its symbol, however many of its aliases match. | | `GET` | `/monomer/{polymertype}/{symbolOrId}` | One monomer. A segment made only of digits is read as an identifier, anything else as a name: a symbol or an alias, matched case-exactly. Pass `by=symbol` or `by=id` to force it. Found by an alias, the response is still the monomer's own record, with its real `symbol`. `version=<n>` on an identifier returns an archived version. | | `PUT` | `/monomer/{polymertype}/{symbol}` | Create or update the monomer with that symbol. Body: a monomer object. `force=true` registers a new monomer even when a deleted one matches; without it the request is refused with `ARCHIVED_MATCH`. `aliases=replace` applies the body's `aliases`; `onStructureConflict=alias` turns a known structure into an alias. See [Aliases](/public/docs/monomer-service-rest-api#aliases). `chiralFlagAbsent` sets how a `molfile` without the chiral flag is read; see [Molfiles and the chiral flag](/public/docs/monomer-service-rest-api#molfiles-and-the-chiral-flag). | | `DELETE` | `/monomer/{polymertype}/{symbolOrId}` | Delete, archiving the state. `force=true` also removes the monomer from its monomer sets. `purge=true` erases the monomer and all its archived versions, irreversibly. `force` and `purge` cannot be combined. | | `GET` | `/monomer/{polymertype}/{id}/versions` | Archived versions, newest first. Empty for a monomer with no history. | | `GET` | `/monomer/{polymertype}/{symbolOrId}/stereoAnalysis` | The stereo analysis of a monomer against the one it was resolved from: `monomer`, `resolvedFrom`, `sameGraph`, `compatible`, `reason`, `resolvedGroups[]`, `centres[]` (one per stereocentre: `parentAtom`, `childAtom`, `element`, `parentLabel`, `childLabel`, `configuration` as `same` or `inverted`, `verdict`) and `warnings[]`. `resolvedFrom` is null and `centres` empty on a monomer with no link. See [Enhanced Stereochemistry](/public/docs/enhanced-stereochemistry). | | `GET` | `/monomer/{polymertype}/{symbolOrId}/stereoisomers` | The registered stereoisomers of a monomer, in the caller's tenant and the public data: `monomer`, `id`, `isomerLabel`, `statement`, `resolvedFrom` and `stereoisomers[]`, one entry per monomer of the same skeleton with `relation` (`enantiomer`, `racemate`, `unknownForm`, `resolvedForm`, `labelSibling`, `diastereomer`, `otherStereoisomer`), `link` (`resolvedFrom`, `resolvedChild` or null), `id`, `symbol`, `name`, `isomerLabel`, `statement` (`abs`, `&`, `or`, `mixed`), `smiles` and `status` (`live` or `retired`). Computed from the stored structures; an empty list for a monomer without stereocentre. See [Enhanced Stereochemistry](/public/docs/enhanced-stereochemistry). | | `POST` | `/monomer/{polymertype}/{id}/revive` | Put a deleted monomer back under its identifier, with its metadata and set memberships. `409` when a live monomer holds its symbol, name or structure. | | `POST` | `/monomer/export` | Export monomers as a file. Body: `format` (`sdf`, `csv`, `xlsx`, `smiles`, `json`), `structure` (`rgroup`, the stored form with its attachment points, or `capped`, each attachment point replaced by its cap group), `delimiter` for CSV, and `monomers` (a list of `{polymerType, id}` or `{polymerType, symbol}`, at most 5000) or `filter` (the list route's parameters). Returns `{format, structure, filename, contentType, exported, skipped, warnings, content}` (`contentBase64` for xlsx). The other form travels in each record as `RGROUP_CXSMILES` or `CAPPED_CXSMILES`; a monomer that cannot be capped is exported in its stored form with a `NOT_CAPPABLE` warning. See [How to Export Monomers](/public/docs/how-to-export-monomers). | ### Monomer object ``` { "symbol": "Deg", "name": "Diethylglycine", "polymerType": "PEPTIDE", "monomerType": "Backbone", "naturalAnalog": "X", "smiles": "CCC(CC)(N[*:1])C([*:2])=O", "author": "M. Curie", "aliases": ["Et2Gly"], "isomerLabel": null, "resolvedFrom": null, "resolvedFromId": null, "rgroups": [ {"label": "R1", "capGroupName": "H", "capGroupSMILES": "[*:1][H]", "alternateId": "R1-H"}, {"label": "R2", "capGroupName": "OH", "capGroupSMILES": "O[*:2]", "alternateId": "R2-OH"} ] } ``` `symbol`, `name`, `polymerType`, `monomerType` and a structure, `smiles` or `molfile`, are required. `id`, `createDate` and `versionNo` are server-owned and ignored on input. `aliases` is always present on output, `[]` when there are none, and lists your organisation's aliases and the public ones; on input it is ignored unless the `PUT` carries `aliases=replace`. `capGroupSMILES` may be omitted: it is derived from the cap group name. See [Monomer Fields](/public/docs/monomer-fields). Three fields carry enhanced stereochemistry; see [Enhanced Stereochemistry](/public/docs/enhanced-stereochemistry): | Field | On input | On output | | --- | --- | --- | | `isomerLabel` | Optional, 10 characters, trimmed. Accepted only on a structure whose canonical key carries an *or* group. Part of the uniqueness rule next to the structure. | Always present, `null` when unset. | | `resolvedFrom` | Optional: the **symbol** of the monomer this one resolves, in your library or in the public one, live or retired. The structure must then be one of the stereoisomers that monomer stands for, or the request is refused with `409`. | The symbol, read from the archive when the parent is retired; `null` when unset. | | `resolvedFromId` | Optional, with `resolvedFrom`: the parent's identifier as read back. Kept as long as it still carries that symbol, so a symbol reused after a retirement cannot repoint the monomer. | The parent's identifier. | A `smiles` is returned as CXSMILES when the structure carries a stereo group, for example `C[C@@H](N[*:1])C([*:2])=O |&1:1|`; a `molfile` as V3000. ### Molfiles and the chiral flag | Query parameter | Values | Effect | | --- | --- | --- | | `chiralFlagAbsent` | `racemic` (default), `absolute` | How a `molfile` in the body is read when it draws stereocentres with the chiral flag off and no `STEABS`, `STERAC` or `STEREL` collection. `racemic`, the MDL standard: all those centres in one `&1` group. `absolute`: as drawn. A molfile with the flag or with collections, and a `smiles`, are read the same either way. Any other value is a `400`. | Accepted on `PUT /monomer/{polymertype}/{symbol}`, `POST /monomer/import` and `POST /structure/sdf/smiles`. See [Enhanced Stereochemistry](/public/docs/enhanced-stereochemistry). A structure stating stereochemistry the library cannot store is refused, with `409` on `PUT` and a `validation:` error on an import row, rather than stored as something else: | Message starts with | Meaning | | --- | --- | | `Enhanced stereochemistry on bonds (the MDLV30/STEB… collection …) is not supported` | The molfile carries a `STEBABS`, `STEBRACn` or `STEBRELn` collection: an E/Z mixture, an unknown double-bond geometry or an atropisomer axis group | | `Axial chirality is not supported (the stereo on atom <n>, …)` | Stereo on a biaryl axis, an allene end or a spiro ring system that the canonical key cannot keep | Validation messages returned with `409`: | Message | Meaning | | --- | --- | | `Symbol already exists` | Creation with a symbol in use | | `Symbol does not exist` | Update of an unknown symbol | | `Duplicate SMILES structure exists` | Same canonical structure as a live monomer of the polymer type | | `Monomer has no Symbol`, `Monomer has no Natural Analog` | Missing field | | `Monomer has wrong Monomertype: <value>` | `monomerType` names no monomer type, for a creation as for an edit | | `Monomer has no R-Groups` | No attachment point in the structure | | `Number of RGroups does not start by one or does not raise correctly` | Labels are not `R1`, `R2`, … without a gap | | `Number of R-Groups in Attachmentlist is NOT equal to number of R-Groups in Smiles` | `rgroups` and the structure disagree | | `Labels of RGroups do not match` | An `rgroups` label has no dummy atom | | `An isomer label ('A') is only allowed on a structure that carries an OR enhanced-stereo group …` | `isomerLabel` on an absolute or racemic structure, or over 10 characters | | `Resolved from 'Phx1': no monomer of that symbol in your library or in the public data` | `resolvedFrom` unknown, or naming the monomer itself | | `Resolved from 'Phx1': not the same molecular graph once stereo is removed …`, `… atom 10 (C) is absolute in the parent and inverted here`, `… the same stereo statement as the parent: nothing is resolved …` | The structure is not one of the stereoisomers the parent stands for. The message names the centre or the rule. | | `The structure of 'Phx1' cannot change: 2 monomer(s) were resolved from it (Phx3, Phx4) …` | A structure change on a monomer that has resolved children. Everything else about it may change. | A successful `PUT` carries an `X-Monomer-Outcome` header: `CREATED`, `UPDATED`, or `ALIASED` when nothing was registered because the symbol became an alias. It may also carry one or more `X-Monomer-Warning` headers, percent-encoded. The write stands in every case: - a resolved monomer settled an *or* group the same way as the child of a labelled sibling, where mirror images were expected; the same text is in `warnings[]` of the stereo analysis; - a stereo group said nothing about the structure, which is a meso form: *The stereo group &1 (atoms 3, 8) says nothing about this structure: inverting its centres gives the same molecule (a meso form), so its centres are registered as absolute.* The monomer is keyed as drawn, so the same compound sent again with or without the group is a duplicate. ### Stereoisomers of a monomer `GET /monomer/PEPTIDE/Phx3/stereoisomers`, for Phx3 registered as the L form resolved from the unknown Phx1 (label A), with Phx4 its D form resolved from Phx2 (label B) and a racemate racPhx: ``` { "monomer": "Phx3", "id": 1203, "isomerLabel": null, "statement": "abs", "resolvedFrom": "Phx1", "stereoisomers": [ { "relation": "enantiomer", "link": null, "id": 1204, "symbol": "Phx4", "name": "D-phenylalanine analogue", "isomerLabel": null, "statement": "abs", "smiles": "O=C([*:2])[C@H](Cc1ccccc1)N[*:1]", "status": "live" }, { "relation": "unknownForm", "link": "resolvedFrom", "id": 1201, "symbol": "Phx1", "name": "Phenylalanine analogue, flask A", "isomerLabel": "A", "statement": "or", "smiles": "O=C([*:2])[C@@H](Cc1ccccc1)N[*:1] |o1:3|", "status": "live" }, { "relation": "unknownForm", "link": null, "id": 1202, "symbol": "Phx2", "name": "Phenylalanine analogue, flask B", "isomerLabel": "B", "statement": "or", "smiles": "O=C([*:2])[C@@H](Cc1ccccc1)N[*:1] |o1:3|", "status": "retired" }, { "relation": "racemate", "link": null, "id": 1200, "symbol": "racPhx", "name": "Phenylalanine analogue, racemate", "isomerLabel": null, "statement": "&", "smiles": "O=C([*:2])[C@@H](Cc1ccccc1)N[*:1] |&1:3|", "status": "live" } ] } ``` The same call on Phx1 lists Phx3 as `resolvedForm` with `link: "resolvedChild"`, Phx4 as `resolvedForm`, Phx2 as `labelSibling` and racPhx as `racemate`. On racPhx, the four others are `resolvedForm`. Live entries come first, then retired ones; within each group the symbols are sorted. A client that suggests a substitution reads `relation` and keeps the entries whose `status` is `live`. ## Aliases A monomer answers to its symbol and to any number of aliases. Your aliases are visible to your organisation only, and may name your own monomers or public ones; public aliases are read-only. Within a polymer type a name, symbol or alias, points to one monomer, case-sensitively, and an alias cannot contain `/`. See [Monomer Aliases](/public/docs/monomer-aliases). | Method | Path | Description | | --- | --- | --- | | `POST` | `/monomer/{polymertype}/{symbolOrId}/aliases` | Add an alias owned by your organisation. Body: `{"alias": "Sar"}`. The monomer is addressed as on `GET`, by identifier, symbol or alias, and may be public. Returns `200` with the monomer and its `aliases`. | | `DELETE` | `/monomer/{polymertype}/{symbolOrId}/aliases/{alias}` | Remove one of your organisation's aliases. Returns `200` with the monomer and its remaining aliases; `404` when your organisation has no alias of that name on the monomer, which is also the answer for a public alias. | `{polymertype}` is `PEPTIDE`, `RNA` or `CHEM` on these paths. An alias is stored trimmed, case kept, and at most 500 characters long. On `PUT /monomer/{polymertype}/{symbol}`: | Query parameter | Effect | | --- | --- | | `aliases=replace` | Your organisation's aliases of the monomer become exactly the body's `aliases`, in the same transaction as the monomer: names missing from the list are removed, `[]` removes them all. Public aliases in the list are ignored, so a body read by `GET` can be sent back unchanged. Without the parameter, the body's `aliases` is ignored. | | `onStructureConflict=alias` | When the structure is already registered under another symbol, the request symbol is added as an alias of that monomer instead of being refused. Nothing else of the body is written. The answer is `200` with `X-Monomer-Outcome: ALIASED` and the existing monomer. The default, `reject`, keeps the `409`. | A refused alias, `409 ALIAS_CONFLICT` or `400` for a `/` or an over-long name, leaves nothing written. `GET /svg/{polymertype}/{name}` also accepts an alias. `DELETE /monomer/...` and the `versions` and `revive` paths do not: they take a symbol or an identifier. HELM validation and conversion in the other Biotoolkit services accept aliases and keep them as written in their output. They read the library through a cache, so a new alias can take up to five minutes to be accepted there. ## Bulk import | Method | Path | Description | | --- | --- | --- | | `POST` | `/monomer/import` | Import a batch. Body: a JSON array of monomer objects, or `multipart/form-data` with a `file` part in JSON, SDF, CSV or XLSX. `format=` overrides the extension. `mode=async` queues the job. `onStructureConflict=alias` registers a known structure under a new symbol as an alias, as on `PUT`. `chiralFlagAbsent` sets how the molfiles are read; see [Molfiles and the chiral flag](/public/docs/monomer-service-rest-api#molfiles-and-the-chiral-flag). | | `GET` | `/monomer/import/{jobId}` | Job status. Redirects with `303` to the result once finished. | | `GET` | `/monomer/import/{jobId}/result` | Per-row outcome, status `207`. | | `GET` | `/monomer/import/capabilities` | The limits of the deployment: row caps, upload size, formats. | A synchronous import returns `207` directly and is limited to 100 rows by default. Above that, or above the upload size, the service answers `413` with the instruction to use `mode=async`. An asynchronous import returns `202` with a `Location` header to poll. An organisation is limited to a few submissions per minute and a few concurrent jobs; `429` and `503` name the limit that was hit. Flat-file conventions for this endpoint: column names are matched ignoring case, spaces, underscores and hyphens; CSV is **semicolon**-delimited; the `rgroups` cell is `R1:H;R2:OH` in one quoted cell; SDF data fields `R1` to `R10` carry the cap group names. Aliases come from an `aliases` column or SDF field, `/`-separated, or a JSON `aliases` array; they are added to the monomer the row lands on, never removed. Unlike the application's import page, `name` and `monomer_type` are required here. Result body: ``` { "rows": [ {"index": 0, "symbol": "Ahp", "status": "CREATED", "errors": []}, {"index": 1, "symbol": "MeNva", "status": "ALREADY_EXISTS_SYMBOL", "errors": ["already-exists: symbol MeNva"]}, {"index": 2, "symbol": "PEG2", "status": "FAILED", "errors": ["validation: Invalid cap group \"OME\" for R2. Valid values: H, OH, NH2, Azide, Ethynyl"]} ], "counts": {"created": 1, "updated": 0, "alreadyExists": 1, "duplicateStructure": 0, "failed": 1, "aliased": 0}, "total": 3 } ``` Rows keep the input order. `ALREADY_EXISTS_SYMBOL` and `DUPLICATE_STRUCTURE` are benign: the monomer is already there. `ALIASED`, with `onStructureConflict=alias`, means the row's symbol was added as an alias of the monomer that has the structure, and is counted in `aliased`. `FAILED` is the only error status; an alias refused for a row fails that row, with an `alias-conflict:` message naming the monomer that holds the name. Error strings start with a category, `parse:`, `validation:`, `conversion:`, `duplicate-structure:`, `already-exists:`, `aliased:`, `alias-conflict:`, `timeout:`, `persist:` or `failed:`, and never contain database or toolkit internals; a reference such as `(ref: 1a2b3c4d5e6f)` lets Discngine support find the detail. Re-submitting a batch is safe: the key is the symbol within the polymer type. ## Cap groups The cap group vocabulary: the groups that can close an attachment point. Your organisation's vocabulary is the public cap groups plus its own, whose codes never repeat a public one. | Method | Path | Description | | --- | --- | --- | | `GET` | `/capgroup` | Your organisation's active vocabulary as `{"capGroups": [{"code": "OH", "name": "Hydroxyl", "template": "O[*:{n}]"}, ...]}`. `template` is the cap's SMILES, with `{n}` standing for the R-group number. | | `GET` | `/capgroup/manage` | Every cap group your organisation owns, active or not, and the active public ones: `code`, `name`, `template`, `description`, `active`, `displayOrder`, `origin` (`system` or `tenant`), `overridesSystem`, and `effective` (the row your organisation resolves for that code). | | `POST` | `/capgroup` | Create a cap group of your organisation. Body: `{"code", "name", "template", "description", "active", "displayOrder"}`. `201` with the row as `/capgroup/manage` lists it and a `Location` of `capgroup/{code}`; `409` when your organisation already has that code, or when it is the code of a public cap group, active or not, compared case-insensitively. | | `PUT` | `/capgroup/{code}` | Replace the name, template, description, `active` and `displayOrder` of one of your organisation's cap groups. The code cannot change: a body `code` that differs from the path is a `400`. `active: true` reactivates it; leaving `active` out keeps its state. | | `DELETE` | `/capgroup/{code}` | Deactivate one of your organisation's cap groups. Returns `{"code": ..., "outcome": "deactivated"}`. | | `DELETE` | `/capgroup/{code}?purge=true` | Delete it instead, only when none of your organisation's monomers uses it: otherwise `409`, with the number of monomers in `monomers`, and the cap group is kept. Deactivate it instead. Returns `{"code": ..., "outcome": "purged"}`. | | `POST` | `/capgroup/refresh` | Reload the service's working copy of the vocabulary, so that a cap group added to it is accepted without a redeployment. Returns `{"Refresh": "valid", "capGroups": <n>, ...}`, or `{"Refresh": "kept", "Reason": ...}` with status `200` when nothing usable came back and the vocabulary already in place was kept. It changes nothing in the vocabulary itself. | Rules for a cap group's fields, the same as on the application's **Cap groups** page: `code` at most 50 characters, without `,`, `;` or `:`, and not `manage` or `refresh`; `name` at most 250 characters, without `,` or `;`; `template` at most 500 characters, carrying `[*:{n}]` exactly once and no space; `description` at most 1000 characters; `displayOrder` an integer or `null`. A field that breaks a rule is a `400` naming the field. The write endpoints address your organisation's cap groups only: a public code, and another organisation's code, are `404`, and the public organisation never writes (`403`). They check no role. Every write reaches the other Biotoolkit services, which reload your vocabulary. See [Attachment Points and CAP Groups](/public/docs/attachment-points-and-cap-groups) and [How to Manage Your Organisation's Cap Groups](/public/docs/how-to-manage-your-organisations-cap-groups). ## Structure conversions Two conversions that write nothing. | Method | Path | Description | | --- | --- | --- | | `POST` | `/structure/sdf/smiles` | The SMILES of each molfile of an SD file, as the service stores structures: canonical, `[*:n]` attachment points, enhanced-stereo block when there is one. Body: the SD file, `text/plain`. `chiralFlagAbsent` as above. Returns `{"chiralFlagAbsent": "racemic", "records": [{"index": 1, "smiles": "..."}, {"index": 2, "error": "..."}]}`, one entry per record in file order; a record is the text before a `$$$$` line. | | `POST` | `/structure/capped` | A structure that is not registered, with each R-group replaced by its cap group: the capping of `GET /monomer/{polymertype}/{id}/capped`, on the body. Body: `{"smiles": "...", "rgroups": [{"label": "R1", "capGroupName": "H"}, ...]}`. Returns `{smiles, molfile, svg}`; `422` with `CAP_GROUP_UNRESOLVED` or `NOT_CAPPABLE` as on the stored route. | ## Images | Method | Path | Description | | --- | --- | --- | | `GET` | `/svg/{polymertype}/{symbol}` | The stored drawing, `image/svg+xml`. The segment may also be an alias. | | `PUT` | `/svg/{polymertype}/{symbol}` | Regenerate the drawing of one private monomer | | `PUT` | `/svg/{polymertype}` | Regenerate the drawings of all private monomers of the type | ## Rules HELM business rules, evaluated by the notation toolkit when a sequence is validated. | Method | Path | Description | | --- | --- | --- | | `GET` | `/rule` | All rules | | `GET` | `/rule/{id}` | One rule | | `PUT` | `/rule` | Create or update the rule in the body | | `DELETE` | `/rule/{id}` | Delete | ## Related - [Import File Format](/public/docs/monomer-import-file-format): the same fields as the columns of an import file - [Versioning and Deletion](/public/docs/monomer-versioning-and-deletion): the semantics of `versions`, `revive`, `force` and `purge`