Template-Store API
Template-Store API
What it is. The template-store owns WIP's document schemas (templates): field definitions, identity_fields, cross-field validation rules, template inheritance (extends), and the entity/reference/relationship usage classes. It registers new templates with the registry to get a template_id, tracks a version history per template_id, and exposes activation (draft → active), deactivation/reactivation, cascade-to-children, and relationship-endpoint widening. (components/template-store/src/template_store/api/templates.py; schemas/template-store.json)
Reaching it. Base path /api/template-store/templates (the service mounts an api_router with prefix /api/template-store and includes the templates router, itself prefixed /templates — components/template-store/src/template_store/api/__init__.py). Every route under /templates carries dependencies=[Depends(require_api_key)] at the router level (templates.py:36-40), and the schema declares a global ApiKeyAuth (X-API-Key header) security scheme (schemas/template-store.json). See also: Auth & Access; Routing & Ingress.
Conventions that apply.
- Bulk-first 200 OK — the three array-bodied endpoints (
POST,PUT,DELETE /templates) accept a list and always respond200 OKwith per-item results inresults[]; a200does not mean every item succeeded (TemplateBulkResponse/TemplateBulkResultItem,schemas/template-store.json). - Mint, not identity-hash natural-upsert — creating a template mints a fresh
template_idvia the registry by default; a template only reuses an existingtemplate_id/versionwhen both are pre-supplied for restore/migration, which skips registry minting and version computation (CreateTemplateRequest.template_id/.versiondescriptions,schemas/template-store.json). Updating a template always targets an explicit, already-knowntemplate_id(UpdateTemplateItem.template_id, required) — this is not document-store's hash-of-identity-fields natural-upsert. - Namespace scoping — every route below is namespace-scoped:
namespaceis required on create (CreateTemplateRequest.namespace), and reads/updates/deletes take an optionalnamespacequery param used to resolve a human-readable value/synonym to a canonicaltemplate_id(e.g.get_template,templates.py:168-196).
See also: API Conventions; PoNIFs.
Endpoints
Listed in router-registration order (templates.py).
POST /api/template-store/templates
- Purpose. Create one or more templates.
- Request. Body: JSON array of
CreateTemplateRequest. Required:value,label,namespace. Notable optional fields:extends/extends_version(parent template + pinned version),identity_fields(array),header_fields(array — bare names projectdata.<name>,metadata.custom.<name>paths allowed, empty falls back toidentity_fields),usage(entitydefault /reference/relationship, immutable after creation),source_templates/target_templates(required whenusage=relationship),versioned(bool, defaulttrue, immutable after creation),fields(array ofFieldDefinition),rules(array ofValidationRule),reporting(ReportingConfig),validate_references(defaulttrue),status(activedefault ordraft, which skips reference validation). Query:on_conflict—error(default) orvalidate; any other value is a top-level400. Underon_conflict=validate: a value collision with an identical schema returnsunchanged; a compatible change (added optional fields only) bumps to version N+1; an incompatible change returns a per-itemerrorwitherror_code=incompatible_schemaand a structured diff indetails. - Response.
200 OK,TemplateBulkResponse— bulk-first:results[](each aTemplateBulkResultItem:index,status,id,value,version,error,error_code,details), plustotal/succeeded/failed. - Errors.
400ifon_conflictis neithererrornorvalidate(top-level, before any item is processed).422on request-shape validation. Registry failures differ by path: with a single item (on_conflict=error, one item), aRegistryErrorbecomes a per-itemerrorresult inside the200; with multiple items oron_conflict=validate, aRegistryErrorbubbles up as a top-level502. - Example.
curl -X POST https://<host>/api/template-store/templates \ -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \ -d '[{"value":"PERSON","label":"Person","namespace":"demo", "fields":[{"name":"first_name","label":"First Name","type":"string","mandatory":true}]}]'{"results":[{"index":0,"status":"created","id":"<template_id>","value":"PERSON","version":1}], "total":1,"succeeded":1,"failed":0}
GET /api/template-store/templates
- Purpose. List templates with pagination and filters.
- Request. Query:
namespace(omit for all),status,extends(parent template value/id — resolved via synonym lookup),value(shows all versions of that value),latest_only(bool, defaultfalse),page(default1,ge=1),page_size(default50,1-1000). - Response.
200 OK,TemplateListResponse:items(TemplateResponse[]),total,page,page_size,pages. - Errors.
422on request-shape validation. - Example.
curl -H "X-API-Key: $API_KEY" \ "https://<host>/api/template-store/templates?namespace=demo&latest_only=true"{"items":[{"template_id":"...","namespace":"demo","value":"PERSON","label":"Person","version":1,"status":"active", "...":"..."}], "total":1,"page":1,"page_size":50,"pages":1}
GET /api/template-store/templates/stamp
- Purpose. Cheap change-detection value for all templates in a namespace, for a caching consumer (e.g. document-store) to decide whether its cached templates are stale, without re-fetching every template. Declared before
/{template_id}specifically so the literal/stamppath isn't captured by the dynamic route (templates.py:149-165). - Request. Query:
namespace(required). - Response.
200 OK,{"namespace": "<namespace>", "stamp": "<count>:<max_updated_at>"}—countcatches creates/deletes,max(updated_at)catches updates and status flips (activate/deactivate/reactivate). - Errors.
422ifnamespaceis missing. - Example.
curl -H "X-API-Key: $API_KEY" \ "https://<host>/api/template-store/templates/stamp?namespace=demo"{"namespace":"demo","stamp":"12:2026-07-01T10:00:00Z"}
GET /api/template-store/templates/{template_id}
- Purpose. Get a template by canonical
template_idor human-readable synonym value, with inheritance resolved (parent fields merged in). - Request. Path:
template_id(UUID or value, e.g."PATIENT"). Query:version(default: latest),namespace(for synonym resolution; also enables a fallback lookup by value). - Response.
200 OK,TemplateResponse. - Errors.
404if not found (after the synonym resolve and, whennamespacegiven, a by-value fallback). Read is gated on the template's actual namespace after resolution — permission denial also surfaces as404, so a caller can't distinguish "doesn't exist" from "exists in a namespace you can't read" (templates.py:183-196). - Example.
curl -H "X-API-Key: $API_KEY" \ "https://<host>/api/template-store/templates/PATIENT?namespace=demo"
GET /api/template-store/templates/{template_id}/raw
- Purpose. Same lookup as above, without inheritance resolution — the template exactly as stored (parent fields not merged in).
- Request. Path:
template_id. Query:version,namespace. - Response.
200 OK,TemplateResponse. - Errors.
404if not found; same namespace-gated-as-404 behavior asGET /{template_id}. - Example.
curl -H "X-API-Key: $API_KEY" \ "https://<host>/api/template-store/templates/PATIENT/raw?namespace=demo"
GET /api/template-store/templates/by-value/{value}
- Purpose. Get the latest version of a template by its value.
- Request. Path:
value. Query:namespace(omit to search all namespaces the caller can access; superadmin unrestricted). - Response.
200 OK,TemplateResponse, inheritance resolved. - Errors.
404if no version found. Always permission-gated: an explicitnamespaceis checked directly; an omitted one restricts the search to the caller's accessible namespaces (templates.py:240-253). - Example.
curl -H "X-API-Key: $API_KEY" \ "https://<host>/api/template-store/templates/by-value/PERSON?namespace=demo"
GET /api/template-store/templates/by-value/{value}/raw
- Purpose. Latest version by value, without inheritance resolution.
- Request. Path:
value. Query:namespace(required — unlike the non-rawby-value routes). - Response.
200 OK,TemplateResponse. - Errors.
404if not found.422ifnamespaceomitted. - Example.
curl -H "X-API-Key: $API_KEY" \ "https://<host>/api/template-store/templates/by-value/PERSON/raw?namespace=demo"
GET /api/template-store/templates/by-value/{value}/versions
- Purpose. All versions of a template by value, newest first.
- Request. Path:
value. Query:namespace(omit for all accessible). - Response.
200 OK,TemplateListResponse(page/page_sizeboth set to the full result count — no real pagination here). - Errors.
404if no versions found. - Example.
curl -H "X-API-Key: $API_KEY" \ "https://<host>/api/template-store/templates/by-value/PERSON/versions?namespace=demo"
GET /api/template-store/templates/by-value/{value}/versions/{version}
- Purpose. A specific version of a template by value.
- Request. Path:
value,version(int). Query:namespace(omit to search all — ambiguous only if the same value exists in more than one namespace). - Response.
200 OK,TemplateResponse, inheritance resolved. - Errors.
404if that value/version isn't found. - Example.
curl -H "X-API-Key: $API_KEY" \ "https://<host>/api/template-store/templates/by-value/PERSON/versions/2?namespace=demo"
GET /api/template-store/templates/{template_id}/versions
- Purpose. All versions of a template by its stable
template_id(no namespace needed —template_idis globally unique and namespace-invariant across its own versions). - Request. Path:
template_id. - Response.
200 OK,TemplateListResponse, newest first. - Errors.
404if not found. Namespace-gated read (using the namespace of any returned version) after lookup. - Example.
curl -H "X-API-Key: $API_KEY" \ "https://<host>/api/template-store/templates/<template_id>/versions"
PUT /api/template-store/templates
- Purpose. Update one or more templates by creating new versions.
- Request. Body: JSON array of
UpdateTemplateItem. Required:template_id. All other fields (value,label,description,extends,extends_version,identity_fields,header_fields,fields,rules,metadata,reporting,updated_by) are optional patches. Query:namespace(for synonym resolution oftemplate_idvalues that are synonyms rather than canonical ids). - Response.
200 OK,TemplateBulkResponse— bulk-first, per-itemstatus/id/value/version/is_new_version. - Errors. Per-item
error(inside the200) onValueError("Template not found" or other) orRegistryError— unlikePOST,PUTnever bubbles aRegistryErrorto a top-level502; it's always folded into the per-item result.422on request-shape validation. Write is gated per item on that template's actual namespace, batched over one lookup (templates.py:364-378). - Example.
curl -X PUT https://<host>/api/template-store/templates \ -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \ -d '[{"template_id":"<template_id>","label":"Person (updated)"}]'{"results":[{"index":0,"status":"updated","id":"<template_id>","value":"PERSON","version":2,"is_new_version":true}], "total":1,"succeeded":1,"failed":0}
GET /api/template-store/templates/{template_id}/dependencies
- Purpose. What depends on a template — child templates that extend it, and documents that use it. Meant to be checked before deactivating.
- Request. Path:
template_id. Query:namespace(for synonym resolution). - Response.
200 OK,TemplateDependencies:child_template_count,child_templates,document_count,has_dependencies,can_deactivate,warning_message. - Errors.
404if the template isn't found (ValueErrorfrom the dependency service). Read gated on the template's actual namespace — this endpoint leaks existence + dependent names, so the gate is checked before the dependency scan runs. - Example.
curl -H "X-API-Key: $API_KEY" \ "https://<host>/api/template-store/templates/<template_id>/dependencies"
DELETE /api/template-store/templates
- Purpose. Soft-delete (deactivate) one or more templates — sets status to
inactive. - Request. Body: JSON array of
DeleteItem. Required:id. Optional:version(default: latest for soft-delete, all for hard-delete),force(bool, defaultfalse— delete even if documents exist),hard_delete(bool, defaultfalse— permanently remove; requires the namespace'sdeletion_modeto befull),updated_by. Query:namespace(synonym resolution). - Response.
200 OK,TemplateBulkResponse— bulk-first. - Errors. Per-item
errorif: another template extends it as a child (blocks unconditionally — "Cannot deactivate: N template(s) extend this template"); dependent documents exist andforceisn't set ("Template has N dependent document(s). Use force=true to deactivate anyway."); or the template isn't found. Write gated per item on the template's actual namespace, batched lookup (templates.py:440-452). - Example.
curl -X DELETE https://<host>/api/template-store/templates \ -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \ -d '[{"id":"<template_id>"}]'{"results":[{"index":0,"status":"deleted","id":"<template_id>"}], "total":1,"succeeded":1,"failed":0}
POST /api/template-store/templates/{template_id}/validate
- Purpose. Validate a template's references without changing anything.
- Request. Path:
template_id. Query:namespace(synonym resolution). Body (optional, defaults shown):ValidateTemplateRequest—check_terminologies(defaulttrue),check_templates(defaulttrue). - Response.
200 OK,ValidateTemplateResponse:valid,template_id,errors[],warnings[], andwill_also_activate— the draft template_ids that would be pulled into the same all-or-nothing set if/activatewere called next, giving a preview of/activate's cascade before you invoke it. - Errors.
404if the template isn't found (via the synonym-resolve-or-404). Read gated on the template's actual namespace — "validating exercises the template's semantics" per the code comment. - Example.
curl -X POST https://<host>/api/template-store/templates/<template_id>/validate \ -H "X-API-Key: $API_KEY"
POST /api/template-store/templates/{template_id}/activate
- Purpose. Activate a draft template (transition
draft→active), validating all references first. - Request. Path:
template_id. Query:namespace(required — write-gated on this namespace before resolution),dry_run(bool, defaultfalse— preview without making changes). - Response.
200 OK,ActivateTemplateResponse:activated[](template ids),activation_details[](ActivationDetail:template_id,value,status—activatedorwould_activateunderdry_run),total_activated,errors[],warnings[]. - Errors.
400on aValueErrorfrom the service (e.g. validation failure). If the template references other draft templates, those are activated too, cascading; the whole set is all-or-nothing — one failing template blocks the entire activation. - Example.
curl -X POST "https://<host>/api/template-store/templates/<template_id>/activate?namespace=demo&dry_run=true" \ -H "X-API-Key: $API_KEY"
POST /api/template-store/templates/{template_id}/reactivate
- Purpose. Reactivate a specific soft-deleted (inactive) version back to active — the symmetric inverse of the
DELETE(deactivate) endpoint. - Request. Path:
template_id. Query:namespace(required, write-gated),version(required — there is no "latest" default;/activateis draft-only and can't address an inactive version). - Response.
200 OK,TemplateResponse. - Errors.
400onValueError— idempotent if the target version is already active; rejected if the target version is a draft (use/activateinstead). - Example.
curl -X POST "https://<host>/api/template-store/templates/<template_id>/reactivate?namespace=demo&version=1" \ -H "X-API-Key: $API_KEY"
POST /api/template-store/templates/{template_id}/cascade
- Purpose. Propagate a parent template's new version onto its direct children, which otherwise keep extending the old parent version after the parent is updated.
- Request. Path:
template_id(the parent). Query:namespace(synonym resolution). - Response.
200 OK,CascadeResponse:parent_template_id,parent_value,parent_version,total,updated,unchanged,failed,results[](CascadeResult:value,old_template_id,new_template_id,new_version,status—updated/unchanged/error,error). Only theextendspointer is updated on each child; child-specific fields are preserved. - Errors.
404if the parent template isn't found.502on aRegistryError. Write gated on the parent template's actual namespace (cascade mutates children). - Example.
curl -X POST "https://<host>/api/template-store/templates/<template_id>/cascade" \ -H "X-API-Key: $API_KEY"
POST /api/template-store/templates/{template_id}/endpoints
- Purpose. Additively widen a relationship (edge-type) template's allowed source/target endpoint templates, in place.
- Request. Path:
template_id(ausage=relationshiptemplate). Query:namespace(required, write-gated). Body:AddEndpointsRequest—add_source_templates[],add_target_templates[](both optional lists of endpoint template values/ids to add). - Response.
200 OK,TemplateResponse(the updated template). - Errors.
400onValueError(e.g. an added value isn't a real template). Additive-only: existing endpoints are never removed; each new endpoint must resolve to a real template; the operation is idempotent. See PoNIF #7 — the frozen-endpoints invariant on a relationship template means "no in-place removal," not "no change." No reindex or reporting migration is triggered (relationship indexes and reporting columns are generic). - Example.
curl -X POST "https://<host>/api/template-store/templates/<template_id>/endpoints?namespace=demo" \ -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \ -d '{"add_source_templates":["PERSON"],"add_target_templates":["ORGANIZATION"]}'
GET /api/template-store/templates/{template_id}/children
- Purpose. Templates that directly extend this template.
- Request. Path:
template_id. Query:namespace(synonym resolution). - Response.
200 OK,TemplateListResponse. - Errors. Read gated on the template's actual namespace ("child listing confirms existence cross-namespace").
- Example.
curl -H "X-API-Key: $API_KEY" \ "https://<host>/api/template-store/templates/<template_id>/children"
GET /api/template-store/templates/{template_id}/descendants
- Purpose. All templates that extend this template, directly or indirectly.
- Request. Path:
template_id. Query:namespace(synonym resolution). - Response.
200 OK,TemplateListResponse. - Errors. Same namespace-gated-read pattern as
/children. - Example.
curl -H "X-API-Key: $API_KEY" \ "https://<host>/api/template-store/templates/<template_id>/descendants"
The schema (schemas/template-store.json) also lists standard operational endpoints tagged Health: GET /, GET /health, GET /api/template-store/health, GET /ready, GET /health/integrity. These aren't part of the Templates resource and their route module isn't under components/template-store/src/**/api/ (this doc's scope), so they're noted here rather than documented as full endpoint entries — see the ⚠ GAP below.
⚠ GAP: the health/readiness route module (root
/,/health,/ready,/health/integrity) isn't in this doc'ssource_scope(onlycomponents/template-store/src/**/api/is), so their auth requirement and implementation can't be verified against code — only their shape as recorded inschemas/template-store.jsonis confirmed.
Store-specific gotchas
- Creating a template mints a fresh
template_idvia the registry by default; a template only reuses a caller-suppliedtemplate_id/versionfor restore/migration, which skips registry minting and version computation entirely (CreateTemplateRequest.template_id/.version). versioned(defaulttrue, immutable after creation) controls whatPUTdoes:truecreates a new version document per update;falseoverwrites the existing record in place. There's no way to change this after the template is created (CreateTemplateRequest.versioned/TemplateResponse.versioned).usage(entity/reference/relationship) is also immutable after creation.usage=referenceis reserved for a future phase and today behaves exactly likeentity— don't rely on any distinct behavior yet (TemplateUsagedescription,schemas/template-store.json).DELETEdeactivates (status →inactive); it does not remove data unlesshard_delete=true, which additionally requires the namespace'sdeletion_modeto be'full'. It's blocked outright if a child template extends the target, and blocked (unlessforce=true) if documents exist referencing it.POST /{template_id}/validate's response includeswill_also_activate— the set of draft templates that would be cascaded into activation together, letting you preview/activate's all-or-nothing cascade before calling it.POST /{template_id}/endpointsonly ever widens a relationship template'ssource_templates/target_templates; it's additive-only and idempotent, and the frozen-endpoints invariant it operates under is "no removal," not "no change" — see PoNIF #7.GET /templates/stampis declared before the dynamic/{template_id}route specifically so the literal pathstampisn't swallowed as atemplate_id.- Value-based lookups are namespace-ambiguous by construction: most
by-value/...routes accept an optionalnamespaceand search across all of the caller's accessible namespaces when it's omitted;by-value/{value}/rawis the one route in this family that requiresnamespace. POST /templatesandPUT /templatesdisagree on how a registry failure surfaces: a single-itemPOST(defaulton_conflict=error) folds aRegistryErrorinto a per-itemerrorinside the200; a multi-itemPOSToron_conflict=validatebubbles the same failure as a top-level502.PUTnever bubbles — it always foldsRegistryErrorinto a per-item result.
See also
- API Conventions
- PoNIFs
- Auth & Access
- Routing & Ingress
- Document-Store API
- Registry API
- Def-Store API
