Ontologies
An ontology is your team's live schema — a completed template instantiated for real product data. Creating one deep-copies the template's structure (regenerating every property id) and automatically creates the root passport at the top level, so you can start adding data immediately. There's no separate "activate" step: an ontology is live the moment it's created.
Creating an ontology regenerates every property id. Always read property
ids from the ontology's levels (below), never from the template you started
with — they won't match.
Both ontologies:read and ontologies:write can be restricted to specific ontologies, and ontologies:read is the root every passport, composition and import ability sits inside: a key cannot hold one of those without it, nor reach an ontology its ontologies:read does not. A scoped ontologies:read lists only its ontologies and reads any other as 403 ability_not_scoped_to_resource. A scoped ontologies:write edits the drafts and metadata of its ontologies but cannot create one, because a new ontology is in no scope yet.
The ontology model
- Name
object- Type
- string
- Description
Always
ontology.
- Name
id- Type
- string
- Description
Unique identifier, e.g.
ontology_00a1b2c3d4e5f607.
- Name
name- Type
- string
- Description
Display name (3–255 characters). Nullable.
- Name
description- Type
- string
- Description
Optional description (up to 1000 characters). Nullable.
- Name
template- Type
- string
- Description
The id of the template this ontology was instantiated from. Nullable.
- Name
levels- Type
- array
- Description
The schema tree — an array of level objects (see below), always the latest version's. This is your reference for level keys and property ids.
- Name
latest_version- Type
- integer
- Description
The highest published version number.
- Name
has_draft- Type
- boolean
- Description
Whether an open draft exists.
- Name
metadata- Type
- object
- Description
Caller-owned metadata bag. Always present,
{}when empty.
- Name
created_at- Type
- string
- Description
ISO 8601 timestamp of creation.
- Name
updated_at- Type
- string
- Description
ISO 8601 timestamp of the last update.
An ontology's passport values are authored in its default language — the team's language when the ontology was created, or English if the team has none — and a passport read falls back to it when the language asked for has no translation. It is not a field of this object; the ontology's composition reports it as default_language.
The level object
Each entry in levels describes one level of the schema. See the Data model for how the type values behave.
- Name
key- Type
- string
- Description
Level key —
Ais the root; children follow breadth-first (B,C, …). This is what you pass aslevelwhen creating passports and imports.
- Name
name- Type
- string
- Description
Human-readable level name. Nullable.
- Name
type- Type
- string
- Description
singular,series, ormany. Nullable.
- Name
granularity- Type
- string
- Description
Which tier this level reports as to the EU registry —
model,batchoritem— ornullwhen the level is internal only. See reporting granularity.
- Name
is_core- Type
- boolean
- Description
Whether this is the level the registry is given as its tier's ID. See which level carries the ID.
- Name
is_root- Type
- boolean
- Description
Whether this is the root level.
- Name
static_lineage- Type
- boolean
- Description
Whether every ancestor of this level is singular (affects how paths are encoded). Nullable.
- Name
parent- Type
- string
- Description
The parent level key, or
nullfor the root.
- Name
children- Type
- array
- Description
Child level keys.
- Name
discoverable- Type
- array
- Description
The adjacent levels traversed when rendering the public passport: the
singularandmanychildren, plus the parent for every non-root level.
- Name
properties- Type
- array
- Description
Property definitions — the same shape as a template's property definitions:
id,name,type, value constraints, anddata_element_idwhen the property is reported. Theidis what you use in passportvaluesand import mappings.
Reporting granularity
A level may declare which of the three tiers it reports as to the EU registry: model, batch or item. null — the default — means the level is internal: it still holds properties and still renders on the passport page, it is simply reported to nobody.
The tiers run coarse to fine, and three rules govern them:
- Containment is strict. An
itemsits inside abatch, abatchinside amodel. A level advances one tier at a time, and only from the tier directly above it. - Only a multiplier opens a finer tier. A
singularlevel is one-to-one with its parent, so it may repeat the tier it inherits but never open a finer one.modelis exempt: it establishes the top of the hierarchy rather than subdividing it, so any level type — a singular root included — may open it. - Each tier is opened once per ontology. Opening means carrying a tier without inheriting it. Two batches on separate branches would leave an item with no answer to which batch it belongs to. Continuing an inherited run is always fine, however deep — and a level may open a tier that a level below it currently opens, which simply extends that run upwards.
A level with no granularity is transparent: it does not interrupt the run around it. A violation is 400 structure_invalid, with the offending level named in error.errors.structure.
Which level carries the ID
Several levels can share a tier, so one of them has to be the one the registry is given as that tier's ID: the level marked is_core. You choose it — which level of your hierarchy is "the item" is a judgement about your business, not something the shape of the tree settles.
- A core must carry a
granularity. - One core per tier. Two would make "the item id" ambiguous.
- It must sit where every path through the run passes through it — at or above any fork. A core chosen down one branch leaves the others reporting nothing. This is checked when you publish a draft, not on every save, so a part-marked draft still saves. Publishing never picks a core for you; template completion does, for a tier only one level carries.
A passport at a level that carries a tier without being its core reports that tier with a null id. And a series below the core holds many instances per core, so its values cannot be reported as part of the entity the core names.
Reported properties
A level's granularity says which tier it reports as; a property's data_element_id says whether that property is reported at all, and under what name the registry knows it.
property
{ "id": "property_9f2k7d", "name": "Capacity", "type": "measurement", "data_element_id": "BATT-BATCH-CAP" }
Absent is the ordinary state, not an omission: most properties on a reported level are your own business rather than the registry's, so only the ones you map are reported. Nothing requires a mapping, and an ontology with none still publishes.
- Only a reported level may carry mappings. A
data_element_idon a level with nogranularitynames a field in a submission that never happens, so it is refused rather than silently dropped. - One property per element, per tier. The registry receives one submission per tier, so two properties offering values for the same element are ambiguous. Across tiers the same element may be reused: a model's weight and an item's weight are separate submissions.
- The value has to survive XML and JSON. Valid UTF-8, at most 128 characters, and no control characters — tabs and newlines included, because XML rewrites those inside an attribute value. Markup characters (
<,&, quotes) are fine; they escape losslessly. Surrounding whitespace is trimmed on write, and a blank value is stored as no mapping.
A violation is 400 structure_invalid, with the offending property named in error.errors.structure.
List ontologies
Lists your team's ontologies as a paginated list. Requires ontologies:read.
A key whose ontologies:read is restricted to specific ontologies gets exactly those. This is the one team-wide list a scoped key may call — it filters rather than refusing — and the creation window, limit and cursor work over the filtered list as they do over the whole team.
Optional attributes
- Name
created[gte]- Type
- string
- Description
Only ontologies created at or after this time. Unix seconds or an ISO date, inclusive of the whole second named.
- Name
created[lte]- Type
- string
- Description
Only ontologies created at or before this time. An inverted range is
400.
- Name
limit- Type
- integer
- Description
Page size, 1–100 (default 10).
- Name
cursor- Type
- string
- Description
A pagination cursor from a previous response's
next_cursor.
The creation window runs as a true key condition, so pages stay exact — this list never comes back short because of the filter. Filters join the cursor's scope; see pagination.
Request
curl -G https://api.synexcloud.com/v1/ontologies \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-d limit=10
Response
{
"object": "list",
"data": [
{
"object": "ontology",
"id": "ontology_00a1b2c3d4e5f607",
"name": "EV Battery Line A",
"description": "2026 production",
"template": "template_00a1b2c3d4e5f607",
"levels": [
{ "key": "A", "name": "SKU", "type": "singular",
"granularity": "model", "is_core": true, "is_root": true }
],
"latest_version": 2,
"has_draft": false,
"metadata": {},
"created_at": "2026-07-10T12:00:00Z",
"updated_at": "2026-07-24T08:31:47Z"
}
],
"has_more": false,
"next_cursor": null,
"url": "/v1/ontologies"
}
Create an ontology
Instantiates a completed template — yours or a public one — into a live ontology. The root passport is created automatically and returned as root_passport; use its id as the parent for your first child passports. Draft templates are rejected with 409 template_is_draft. Accepts an optional Idempotency-Key header.
Requires an ontologies:write that is not restricted to specific ontologies — a new ontology is in no scope yet, so a scoped one is 403 ability_not_scoped_to_resource. A team without a company (a supplier team) is refused with 403 company_required, because the ontology comes with its root passport and such a team issues none.
Required attributes
- Name
template- Type
- string
- Description
The completed template's id.
- Name
name- Type
- string
- Description
Display name, 3–255 characters.
Optional attributes
- Name
description- Type
- string
- Description
Description, up to 1000 characters.
- Name
metadata- Type
- object
- Description
Initial metadata bag.
Request
curl https://api.synexcloud.com/v1/ontologies \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "template": "template_00a1b2c3d4e5f607", "name": "EV Battery Line A", "description": "2026 production" }'
Response
{
"object": "ontology",
"id": "ontology_00a1b2c3d4e5f607",
"name": "EV Battery Line A",
"description": "2026 production",
"template": "template_00a1b2c3d4e5f607",
"levels": [
{ "key": "A", "name": "SKU", "type": "singular", "granularity": null,
"is_core": false, "is_root": true, "children": ["B"], "properties": [] },
{ "key": "B", "name": "Pack", "type": "series", "granularity": null,
"is_core": false, "parent": "A",
"properties": [
{ "id": "property_9f2k7d", "name": "Capacity", "type": "measurement" }
] }
],
"latest_version": 1,
"has_draft": false,
"metadata": {},
"root_passport": {
"object": "passport",
"id": "pass_00a1b2c3d4e5f607",
"ontology": "ontology_00a1b2c3d4e5f607",
"parent": null,
"level": "A",
"path": "A",
"friendly_id": null,
"ontology_version": 1,
"publish_status": null,
"publish_status_resolved": "draft",
"release_number": 0,
"values": {},
"metadata": {},
"created_at": "2026-07-10T12:00:00Z",
"updated_at": "2026-07-10T12:00:00Z"
},
"created_at": "2026-07-10T12:00:00Z",
"updated_at": "2026-07-10T12:00:00Z"
}
Retrieve an ontology
Returns the ontology including its full levels — level keys, types, and property definitions. This is where you read the property ids you'll write to in passport values and reference in import mappings. Requires ontologies:read.
A key whose ontologies:read is restricted to specific ontologies reads only those; any other id is 403 ability_not_scoped_to_resource whether or not it exists — the scope is decided before the lookup. An unscoped key sees 404 for another team's ontology. The same scope check guards every route under the ontology — versions, draft and metadata.
levels here is always the latest version's. To resolve an older passport's schema, read its pinned version with GET /v1/ontologies/{ontology}/versions/{version}.
Request
curl https://api.synexcloud.com/v1/ontologies/ontology_00a1b2c3d4e5f607 \
-H "Authorization: Bearer $SYNEX_API_KEY"
Response
{
"object": "ontology",
"id": "ontology_00a1b2c3d4e5f607",
"name": "EV Battery Line A",
"description": "2026 production",
"template": "template_00a1b2c3d4e5f607",
"levels": [
{
"key": "A", "name": "SKU", "type": "singular",
"granularity": "model", "is_core": true, "is_root": true,
"static_lineage": true, "parent": null, "children": ["B"],
"discoverable": [], "properties": []
},
{
"key": "B", "name": "Pack", "type": "series",
"granularity": "batch", "is_core": true, "is_root": false,
"static_lineage": true, "parent": "A", "children": [],
"discoverable": ["A"],
"properties": [
{ "id": "property_9f2k7d", "name": "Capacity", "type": "measurement",
"data_element_id": "BATT-BATCH-CAP" }
]
}
],
"latest_version": 2,
"has_draft": false,
"metadata": {},
"created_at": "2026-07-10T12:00:00Z",
"updated_at": "2026-07-24T08:31:47Z"
}
Update an ontology
Updates the metadata bag and nothing else. Requires ontologies:write.
That is the whole endpoint. An ontology's name, description and structure come from its template and change only through the draft/version lifecycle, where every edit is checked against the passports already issued under the old schema. This is not a second, unversioned way in.
metadata must be present in the body — an empty body is 400 parameter_invalid naming metadata — so send {} for a deliberate no-op.
Required attributes
- Name
metadata- Type
- object
- Description
Merged per key; a null value deletes that key.
Request
curl -X PATCH https://api.synexcloud.com/v1/ontologies/ontology_00a1b2c3d4e5f607 \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"metadata": {"erp_line": "L-14"}}'
Response
{
"object": "ontology",
"id": "ontology_00a1b2c3d4e5f607",
"name": "EV Battery Line A",
"description": "2026 production",
"template": "template_00a1b2c3d4e5f607",
"levels": [
{ "key": "A", "name": "SKU", "type": "singular",
"granularity": "model", "is_core": true, "is_root": true }
],
"latest_version": 2,
"has_draft": false,
"metadata": { "erp_line": "L-14" },
"created_at": "2026-07-10T12:00:00Z",
"updated_at": "2026-07-28T14:02:19Z"
}
Versioning
A schema evolves without breaking the passports already issued under it. Versions are append-only: publishing adds version N+1 and never alters or removes an existing one.
Every passport is pinned to the version it was created under, exposed as ontology_version. That pin decides two things:
- Reads render the passport against its own version's levels — its
discoverable_levels, discovery chain and property definitions are the ones that existed when it was issued. PATCH /v1/passports/{passport}validates values against its own version's property definitions. A property added in a later version is rejected on an older-pinned passport.
Creation surfaces use the latest version instead. Creating a passport or
starting an import validates against latest_version, and the new
passport pins that version. This is deliberate: it means a level added in a
new version is immediately creatable under a parent still pinned to an old
one.
Changes must be additive
The draft is validated against the published version before it can be saved or published. A violation is 400 structure_invalid, with one message per violation in error.errors.structure:
- published levels cannot be deleted, re-parented, re-typed, or re-lettered;
- a published
granularitycannot change or be removed — but a level that has none may be given one in a later version, which is how an ontology already in production starts reporting without being replaced; - a published
is_corecannot be taken away: it is the level the registry was given as that tier's ID; - a published
data_element_idcannot change or be withdrawn, since the registry may already hold values reported under it — but a property published without one may be mapped in a later version; - a published parent/child edge cannot be dropped;
- a property carried over from a published version must keep its
id; - the graph stays a single tree rooted at
A.
Within those rules, adding levels and adding, editing or removing properties are all fine.
Level keys are assigned by the server
This is the part that most often surprises people. You do not choose level keys, and you do not choose ids for new properties.
- Send any placeholder key you like for a new level. The server assigns the next unused letter —
C,D, …AA— and returns it in the response'slevels. Read the real key back from there; the placeholder is gone. - A key that already exists in the published schema or the current draft is kept as-is. Existing levels are never re-lettered — every passport path in the system depends on that.
- One trap: a placeholder that looks like a level key (
^[A-Z]+$) but is not one of the known keys is treated as a placeholder and silently re-lettered. Use obviously non-key placeholders ("new-module","tmp-1") so you never have to think about it. - Property ids work the same way: omit
idfor a new property and one is minted; carry an existingidthrough verbatim to keep the values already stored against it.
Concurrency
Three guards, three different 409s:
| Code | Meaning | What to do |
|---|---|---|
draft_exists | POST /draft when one is already open | GET it, or DELETE it first |
draft_conflict | PATCH /draft with a stale base_updated_at | Re-read the draft, reapply your changes |
version_conflict | publish with a stale expected_latest_version | Re-read latest_version and retry |
base_updated_at is an opaque token, not a timestamp you should read. Echo it back exactly as received; do not parse, reformat, or compare it. Every draft response carries a fresh one.
PATCH /draft is deliberately not covered by idempotency keys — base_updated_at is already its concurrency control. The two POSTs are: note that a replayed POST /draft returns the stored response, whose base_updated_at may since have gone stale, so if a following PATCH returns draft_conflict, re-read the draft with GET.
Ontologies from before versioning. An ontology whose stored schema
predates versioning reports latest_version: 1 and a single version 1 with a
null published_at. You can open and edit a draft on it, but publishing is
refused with 409 ontology_not_migrated until it has been backfilled.
Ontology and version mutations are recorded on the change feed as ontology.created, ontology.updated and ontology.version.published. Opening, editing and discarding a draft record nothing.
List versions
Every published version of the ontology, oldest first. Requires ontologies:read.
Each entry is a summary — its number, when it was published, and whether it is the version levels currently reflects. Read a version's schema with retrieve a version. Versions are immutable once published, so this list only ever grows, and it always comes back whole: has_more is false.
Request
curl https://api.synexcloud.com/v1/ontologies/ontology_00a1b2c3d4e5f607/versions \
-H "Authorization: Bearer $SYNEX_API_KEY"
Response
{
"object": "list",
"data": [
{
"object": "ontology.version",
"version": 1,
"published_at": "2026-07-10T12:00:00Z",
"latest": false
},
{
"object": "ontology.version",
"version": 2,
"published_at": "2026-07-24T08:31:47Z",
"latest": true
}
],
"has_more": false,
"next_cursor": null,
"url": "/v1/ontologies/ontology_00a1b2c3d4e5f607/versions"
}
Retrieve a version
One published version, with the schema exactly as passports pinned to it read it. Requires ontologies:read. A number outside 1 to latest_version is 404.
This is how you resolve an older passport's schema: take its ontology_version and read that version's levels.
Request
curl https://api.synexcloud.com/v1/ontologies/ontology_00a1b2c3d4e5f607/versions/1 \
-H "Authorization: Bearer $SYNEX_API_KEY"
Response
{
"object": "ontology.version",
"ontology": "ontology_00a1b2c3d4e5f607",
"version": 1,
"status": "published",
"published_at": "2026-07-10T12:00:00Z",
"levels": [
{
"key": "A", "name": "SKU", "type": "singular",
"granularity": null, "is_core": false, "is_root": true,
"static_lineage": true, "parent": null, "children": ["B"],
"discoverable": [], "properties": []
},
{
"key": "B", "name": "Pack", "type": "series",
"granularity": null, "is_core": false, "is_root": false,
"static_lineage": true, "parent": "A", "children": [],
"discoverable": ["A"],
"properties": [
{ "id": "property_9f2k7d", "name": "Capacity", "type": "measurement" }
]
}
]
}
The draft lifecycle
An ontology has at most one open draft at a time, always based on the latest published version.
- Open a draft — a copy of the latest published version.
- Edit it, sending the complete level map each time.
- Publish it as the next version, or delete it to throw the work away.
Deleting a draft never affects published versions.
Retrieve the draft
Reads the open draft. Requires ontologies:read. 404 draft_missing when none is open.
Every draft response carries a fresh base_updated_at — echo it back verbatim on your next PATCH.
Request
curl https://api.synexcloud.com/v1/ontologies/ontology_00a1b2c3d4e5f607/draft \
-H "Authorization: Bearer $SYNEX_API_KEY"
Response
{
"object": "ontology.draft",
"ontology": "ontology_00a1b2c3d4e5f607",
"status": "draft",
"created_from_version": 2,
"base_updated_at": "1753699847.482913",
"latest_version": 2,
"levels": [
{ "key": "A", "name": "SKU", "type": "singular", "granularity": "model",
"is_core": true, "is_root": true, "children": ["B"], "properties": [] },
{ "key": "B", "name": "Pack", "type": "series", "granularity": "batch",
"is_core": true, "parent": "A", "children": [],
"properties": [
{ "id": "property_9f2k7d", "name": "Capacity", "type": "measurement",
"data_element_id": "BATT-BATCH-CAP" }
] }
]
}
Open a draft
Copies the latest published version into a new draft and returns it with 201. Requires ontologies:write. Accepts an optional Idempotency-Key header.
409 draft_exists if one is already open — GET it, or DELETE it first.
A replayed POST returns the stored response, whose base_updated_at may since have gone stale. If a following PATCH returns draft_conflict, re-read the draft with GET.
Request
curl -X POST https://api.synexcloud.com/v1/ontologies/ontology_00a1b2c3d4e5f607/draft \
-H "Authorization: Bearer $SYNEX_API_KEY"
Response
{
"object": "ontology.draft",
"ontology": "ontology_00a1b2c3d4e5f607",
"status": "draft",
"created_from_version": 2,
"base_updated_at": "1753699847.482913",
"latest_version": 2,
"levels": [
{ "key": "A", "name": "SKU", "type": "singular", "granularity": "model",
"is_core": true, "is_root": true, "children": ["B"], "properties": [] },
{ "key": "B", "name": "Pack", "type": "series", "granularity": "batch",
"is_core": true, "parent": "A", "children": [],
"properties": [
{ "id": "property_9f2k7d", "name": "Capacity", "type": "measurement",
"data_element_id": "BATT-BATCH-CAP" }
] }
]
}
Edit the draft
Replaces the draft's structure. Requires ontologies:write.
Send the complete level map, not a partial patch. structure replaces
the draft's whole tree — a level you omit is a level you deleted, and a
granularity, is_core or data_element_id you leave off a published
level or property reads as withdrawing it. The additive-change validator
rejects both.
Changes are validated against the published version on every save, together with the granularity and data element rules, so a violation surfaces here rather than at publish time. Whether every reported tier has its one reachable core is the exception: that is checked when you publish.
Required attributes
- Name
base_updated_at- Type
- string
- Description
The opaque token from the previous draft response, echoed verbatim. A mismatch is
409 draft_conflict.
- Name
structure- Type
- object
- Description
The complete level map, keyed by level key. Each level carries
name,type,is_root,childrenandproperties, plusgranularityandis_corewhen it reports. New levels may use any placeholder key — the server assigns the real one.
Request
curl -X PATCH https://api.synexcloud.com/v1/ontologies/ontology_00a1b2c3d4e5f607/draft \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"base_updated_at": "1753699847.482913",
"structure": {
"A": {"name": "SKU", "type": "singular", "is_root": true,
"granularity": "model", "is_core": true,
"properties": [], "children": ["B", "new-module"]},
"B": {"name": "Pack", "type": "series", "children": [],
"granularity": "batch", "is_core": true,
"properties": [
{"id": "property_9f2k7d", "name": "Capacity", "type": "measurement",
"data_element_id": "BATT-BATCH-CAP"}
]},
"new-module": {"name": "Module", "type": "many", "children": [],
"properties": [{"name": "Slot", "type": "short_text"}]}
}
}'
Response
{
"object": "ontology.draft",
"ontology": "ontology_00a1b2c3d4e5f607",
"status": "draft",
"created_from_version": 2,
"base_updated_at": "1753700114.907255",
"latest_version": 2,
"levels": [
{ "key": "A", "name": "SKU", "type": "singular", "granularity": "model",
"is_core": true, "is_root": true, "children": ["B", "C"], "properties": [] },
{ "key": "B", "name": "Pack", "type": "series", "granularity": "batch",
"is_core": true, "parent": "A", "children": [],
"properties": [
{ "id": "property_9f2k7d", "name": "Capacity", "type": "measurement",
"data_element_id": "BATT-BATCH-CAP" }
] },
{ "key": "C", "name": "Module", "type": "many", "granularity": null,
"is_core": false, "parent": "A", "children": [],
"properties": [
{ "id": "property_4c8b1e", "name": "Slot", "type": "short_text" }
] }
]
}
The placeholder new-module came back as level C, with a minted property_4c8b1e. Read both from the response — the values you sent are not authoritative.
Discard the draft
Throws the draft away. Requires ontologies:write. Published versions are never affected. 404 draft_missing when none is open.
Request
curl -X DELETE https://api.synexcloud.com/v1/ontologies/ontology_00a1b2c3d4e5f607/draft \
-H "Authorization: Bearer $SYNEX_API_KEY"
Response
{
"object": "ontology.draft",
"ontology": "ontology_00a1b2c3d4e5f607",
"deleted": true
}
Publish the draft
Turns the draft into version N+1 and closes it. Requires ontologies:write. Accepts an optional Idempotency-Key header.
expected_latest_version is the concurrency guard: pass the latest_version you built the draft against, and a mismatch is 409 version_conflict. Re-read latest_version and retry.
Publishing re-runs the additive-change checks and adds one of its own: every reported tier must have exactly one is_core level, placed where every path through its run passes through it. Until it does, publishing fails with 400 structure_invalid — it never picks a core for you.
Publishing emits ontology.version.published on the change feed; the snapshot carries the version payload including its number.
Existing passports are untouched — they stay pinned to the version they were created under. New passports created from now on pin the new one.
Required attributes
- Name
expected_latest_version- Type
- integer
- Description
The version number you expect to be current. A mismatch is
409 version_conflict.
Request
curl -X POST https://api.synexcloud.com/v1/ontologies/ontology_00a1b2c3d4e5f607/draft/publish \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"expected_latest_version": 2}'
Response
{
"object": "ontology.version",
"ontology": "ontology_00a1b2c3d4e5f607",
"version": 3,
"status": "published",
"published_at": "2026-07-28T14:20:05Z",
"levels": [
{ "key": "A", "name": "SKU", "type": "singular", "granularity": "model",
"is_core": true, "is_root": true, "children": ["B", "C"], "properties": [] },
{ "key": "B", "name": "Pack", "type": "series", "granularity": "batch",
"is_core": true, "parent": "A",
"properties": [
{ "id": "property_9f2k7d", "name": "Capacity", "type": "measurement",
"data_element_id": "BATT-BATCH-CAP" }
] },
{ "key": "C", "name": "Module", "type": "many", "granularity": null,
"is_core": false, "parent": "A",
"properties": [
{ "id": "property_4c8b1e", "name": "Slot", "type": "short_text" }
] }
]
}