Skip to content

API Reference

Authorization

/admin routes need a JWT and an OPA decision. The action is derived from the path and the HTTP method, so reads and writes are separate grants. Only the route's fixed segments are read, each at its position: a member key, asset key or any other id containing lookup, import, export, profile, name, role, area or delivery-points never changes the action, nor does the body or the query, and a path matching no route derives admin, which only rec-registry.admin satisfies (REQ-0065).

Action Reached by Scope
read any GET under /admin rec-registry.read
members.write create, the general PATCH, …/status and DELETE on …/members… rec-registry.members.write
members.profile.write PATCH …/members/{key}/profile (role and area only) rec-registry.members.profile.write, or rec-registry.members.write
members.name.write PUT …/members/{key}/name rec-registry.members.name.write, or rec-registry.members.write
members.role.write PUT …/members/{key}/role rec-registry.members.role.write, rec-registry.members.profile.write, or rec-registry.members.write
members.area.write PUT …/members/{key}/area rec-registry.members.area.write, rec-registry.members.profile.write, or rec-registry.members.write
members.delivery_points.write PUT and DELETE …/members/{key}/delivery-points/{id}, ?replaces= included rec-registry.members.delivery_points.write, or rec-registry.members.write
members.purge DELETE …/members/{key}?purge=true rec-registry.members.purge
assets.write write methods on …/assets… rec-registry.assets.write
community.write write methods on a community, its areas (the area rename included) or its topology nodes rec-registry.community.write
import / export /admin/import*, /admin/export rec-registry.import / .export
lookup /admin/lookup/* rec-registry.lookup
assets.lookup /admin/lookup/assets-by-user-ids, /admin/lookup/members-by-dids rec-registry.lookup

rec-registry.admin satisfies all of them (the shared matcher treats {service}.admin as covering {service}.*), so existing tokens keep working — but do not give it to a service account. Grant the actions it calls: a service that registers approved participants needs members.write, one that attaches meters needs assets.write, one that corrects a member's area needs members.area.write — which reaches that one route and nothing else (ADR-0011) — and none has any business importing, exporting or purging. A member field is granted alone; identity (user_id, did, status, creating a member) is members.write only.

Interactive OpenAPI docs are available at http://localhost:8004/docs under CELINE_ENV=dev, or with CELINE_PUBLIC_DOCS=true (REQ-0092).

Refusal codes

A refusal a caller is expected to act on answers

{"detail": "This sensor is already held by another active member", "code": "sensor_held"}

detail is a sentence for people and stays a string; branch on code, never on the wording (REQ-0073). The OpenAPI document names the body ErrorResponse and the codes ErrorCode.

Code Status Refusal
community_not_found 404 a write naming an unknown community
member_not_found 404 a write naming a member the community does not have
asset_not_found 404 deleting an asset the member does not hold
member_key_taken 409 the member key is taken in the community
user_id_taken 409 the user_id is taken in the community
did_taken 409 the DID is held by another active member anywhere (REQ-0096)
asset_key_taken 409 the asset key is held by another member of the community
asset_key_too_long 422 the asset key is longer than 128 characters (asset PUT, member create, import)
sensor_held 409 (422 on an import) another active member, in any community, holds the sensor
area_in_use 409 deleting an area members still reference
invalid_status 422 a status outside pending, active, suspended, inactive (every member write, import)
invalid_role 422 a role outside consumer, prosumer, producer, operator, admin (every member write, import)
unknown_area 422 an area that is not a key of the community's areas (every member write; on an import, the bundle's own areas)
invalid_area_boundary 422 an area that is not one primary substation: one boundary, one primary_substation node with its id, no two areas on one boundary id (area PUT, topology node PUT, import)
topology_node_in_use 409 deleting a topology node an area still references, or another node names as its parent
area_not_found 404 renaming an area the community does not have
area_key_taken 409 renaming an area onto a key the community already has
invalid_area_key 422 an area key that is not letters, digits, - and _, starting with a letter or digit, at most 128 characters (area PUT, rename's new_key, import)
not_a_member 403 a /user route other than GET /user, for a caller whose username names no active member
ambiguous_member 409 a /user route, for a caller active in more than one community whose token's organizations do not narrow it to one; a lookup that more than one active member would answer (REQ-0097 – REQ-0099)
delivery_point_held 409 (422 on an import) another active member, in any community, holds the delivery point (trimmed, case-insensitive) — as a point, or as a meter's pod (REQ-0093)
delivery_point_linked 409 deleting a delivery point one of the member's meters still names as its pod

Other refusals — FastAPI's validation errors, a body id or key that does not match the path, the import's force guard, a 404 for an unknown delivery point (on its DELETE, or as the replaces of a correction), topology node, or area on its DELETE — keep the plain {"detail": ...} body. A route that answers a coded 422 documents its 422 as oneOf ErrorResponse and HTTPValidationError (whose detail is a list): both arrive with that status.

The access log

A meter's asset key is meter-<sensor_id>, every member route takes the member key in the path, three routes a sensor id, three a user id and three a delivery-point id, so the access line uvicorn writes is rewritten before it is logged (REQ-0080): method, route shape and status stay, the identifiers do not.

Request Logged as
PUT /admin/communities/example-rec/members/ex-00001/assets/meter-SEN-1 PUT /admin/communities/example-rec/members/{member_key}/assets/{asset_key}
PUT /admin/communities/example-rec/members/ex-00001/name PUT /admin/communities/example-rec/members/{member_key}/name
GET /admin/lookup/asset-by-sensor-id/SEN-1 GET /admin/lookup/asset-by-sensor-id/{sensor_id}
GET /admin/lookup/member-by-user-id/user-1 GET /admin/lookup/member-by-user-id/{user_id}
GET /admin/lookup/community-by-delivery-point/DP-1 GET /admin/lookup/community-by-delivery-point/{dp_id}
GET /admin/communities/example-rec/meters?limit=50&cursor=meter-SEN-1 GET /admin/communities/example-rec/meters?limit=50&cursor={redacted}

Every member key is replaced, on every /members/{member_key} route, and every asset key, on every /assets/{asset_key} route; so is everything after by-sensor-id/, by-user-id/, /delivery-points/by-id/ and community-by-delivery-point/, the delivery-point id on the member delivery-point PUT and DELETE, the cursor of the member, asset, meter and delivery-point listings, and any owner, sensor_id(s), user_id(s), dp_id(s) or delivery_point_id(s) query value. The markers are fixed rather than a hash of the id. Only uvicorn's own line is covered: a proxy in front of the service that logs the URL logs the identifiers.


User Routes

Self-service endpoints scoped to the authenticated user's membership. Prefix: /user.

Only an active member answers (REQ-0094): a pending, suspended or inactive row counts as no row. A caller whose username names no active member gets GET /user with membership: null; every other route answers 403 with {"detail": "You are not a member of any community", "code": "not_a_member"} (REQ-0047). Branch on the code, not the sentence. A caller active in more than one community is answered from the one their token's organization aliases name; when those do not name exactly one, every route answers 409 ambiguous_member (REQ-0095).

GET /user

Profile summary: user info, community membership, asset counts by type.

GET /user/member

Own member detail (key, name, role, area, status, delivery points).

GET /user/community

Detail of the community the user belongs to.

GET /user/assets

List own assets.

Query params: - asset_type — filter by asset type (pv, meter, storage, ev_charger, heat_pump, load)

GET /user/assets/{asset_key}

Detail of a specific owned asset.

GET /user/delivery-points

List own delivery points.


Admin Routes — Communities

Community browsing and detail endpoints. Prefix: /admin.

GET /admin/communities

Paginated list of communities.

Query params: - key — filter by community key - limit — page size (default 50, max 500) - cursor — pagination cursor

GET /admin/communities/{community_key}

Community detail including areas, topology, legal, contact, settings.

GET /admin/communities/{community_key}/topology

Grid topology nodes for a community, each {id, type, name, operator_id, parent, area} — the bundle's names. (Before 1.6.0 reads answered operator, always null, and never the stored operator_id.)

GET /admin/communities/{community_key}/members

Paginated list of members.

Query params: - role — filter by role (consumer, prosumer, producer, operator, admin) - status — filter by status (pending, active, suspended, inactive) - area — filter by area key - limit, cursor — pagination

GET /admin/communities/{community_key}/members/{member_key}

Member detail.

GET /admin/communities/{community_key}/members/by-user-id/{user_id}

Lookup member by user ID within a community.

GET /admin/communities/{community_key}/members/{member_key}/delivery-points

Delivery points for a specific member.

GET /admin/communities/{community_key}/delivery-points

Paginated list of all delivery points in a community.

Query params: - type — filter by delivery point type - active — filter by active status - limit, cursor — pagination

GET /admin/communities/{community_key}/delivery-points/duplicates

The community's delivery points that more than one active member holds — the per-community view of celine-rec-registry duplicate-delivery-points, for an operator console. A read (rec-registry.read). Not paginated.

{
  "community_key": "example-rec",
  "items": [
    {
      "delivery_point": "it001e00000001",
      "holders": [
        {"member_key": "ex-00001", "id": "IT001E00000001"},
        {"member_key": "ex-00002", "id": "it001e00000001"}
      ],
      "held_elsewhere": 1,
      "active_holders": 3
    }
  ]
}

delivery_point is the compared form (trimmed, lower-cased); holders are this community's active holders with the spelling each stored; held_elsewhere counts active holders in other communities, which are never named, nor their communities. A point shared only between other communities is not listed. Unknown community: 404.

GET /admin/communities/{community_key}/delivery-points/by-id/{dp_id}

Lookup a specific delivery point by its ID.

GET /admin/communities/{community_key}/assets

Paginated list of community assets.

Query params: - asset_type — filter by type - owner — filter by member key - limit, cursor — pagination

GET /admin/communities/{community_key}/assets/{asset_key}

Asset detail.

GET /admin/communities/{community_key}/assets/by-sensor-id/{sensor_id}

Lookup asset by sensor ID within a community.

GET /admin/communities/{community_key}/meters

Convenience endpoint listing meter-type assets with POD and meter type info.

Query params: - owner — filter by member key - limit, cursor — pagination


Admin Routes — Lookup

Cross-community lookups. Prefix: /admin/lookup.

Every lookup below except members-by-dids answers from active members only (REQ-0097 – REQ-0099): a released (inactive), pending or suspended member is not found, the same as an unknown id, and contributes no rows to a batch. Where more than one active member would answer a single lookup, it is 409 ambiguous_member, naming nobody, rather than whichever row came first.

GET /admin/lookup/community-by-user-id/{user_id}

Find which community a user belongs to.

GET /admin/lookup/community-by-sensor-id/{sensor_id}

Find which community owns a given sensor.

GET /admin/lookup/community-by-delivery-point/{dp_id}

Find which community a delivery point belongs to.

GET /admin/lookup/member-by-user-id/{user_id}

Lookup member details by user ID across communities.

GET /admin/lookup/asset-by-sensor-id/{sensor_id}

Lookup asset details by sensor ID across communities.

POST /admin/lookup/assets-by-sensor-ids

Batch lookup: resolve multiple sensor IDs to assets in a single request.

Request body: {sensor_ids: [...]}, at most 500.

POST /admin/lookup/assets-by-user-ids

Batch lookup: resolve the assets owned by a set of members, across communities. Every row carries owner_user_id, so the caller can attribute it back to the member it asked about. A user id that is an active member of more than one community refuses the whole batch with 409 ambiguous_member (REQ-0097).

Request body: {user_ids: [...]}, at most 500.

POST /admin/lookup/members-by-dids

Batch lookup: resolve the members holding a set of dataspace DIDs, across communities. Every row carries its did, plus the member's user_id, delivery points and community.

Members, not assets — deliberately. A participant is registered with a declared supply point and no asset at all, because a meter's sensor_id is assigned at physical installation. An asset-shaped answer would be empty for everyone whose meter is not commissioned yet; a commissioned meter stays reachable through assets-by-user-ids and the user_id in the same row.

Request body: {dids: [...]}, at most 500.

All three batch routes share one bound, and none of them is an enumeration oracle: an identifier matching nothing contributes no row and is never a 404.


Admin Routes — Management

Import/export operations. Prefix: /admin.

POST /admin/import

Import a community from a JSON bundle. Full replace: deletes existing community graph and recreates from bundle. Atomic operation.

Destructive. Overwriting a community that already exists requires force: true and otherwise answers 409, naming the members and assets that would have been deleted. See Import & Export.

Request body: {bundle, dry_run, force} (see Import & Export).

Response: ImportReport with created counts. A dry run also lists in refusals ([{code, detail}]) every invariant refusal the import would make.

Refused whole, before anything is deleted, with 422 and the invariant's code, when the bundle breaks an invariant the runtime writes keep — today sensor_held: one trimmed sensor id held by two active members of the bundle, or by one of them and an active member of another community (that community and member are not named); asset_key_too_long: an asset key over 128 characters; invalid_role, invalid_status, unknown_area: a member whose role or status is outside its set, or whose area is not a key of the bundle's community.areas; invalid_area_key: an area whose key is not an area key (below, under the area PUT); invalid_area_boundary: an area that is not one primary substation (below, under the area PUT), judged against the bundle's own community.topology — so a bundle written before schema v0.7 that has areas is refused, whatever schema_version it declares. The replaced community's own current rows do not count.

POST /admin/import/yaml

Import one or more communities from a YAML multidocument body. Each document is a separate community bundle.

Request body: text/yaml — multidocument YAML. Query: dry_run, force.

Response: MultiImportReport with per-community results. A bundle breaking an invariant refuses the whole request as POST /admin/import does.

GET /admin/export

Export communities as YAML multidocument.

Response: text/plain — YAML bundle(s).


Admin Routes — Writes

Runtime changes to a community. Prefix: /admin.

Every route here keeps one rule: no write reduces a sibling. PUT on a member replaces that member, not the member list; patching a member does not clear its delivery points; upserting an area does not drop the others. The only endpoint that deletes what it was not given is the bundle import above.

POST /admin/communities/{community_key}/members

Create one member, with its delivery points and assets.

key is optional — when omitted it is minted from the community's own numbering (ex-00001 → ex-00002), so a caller with no opinion still gets a key that reads correctly in an exported bundle.

did is optional and unique across the whole registry, unlike key and user_id, which are unique per community. It is usually written afterwards rather than here — see PATCH below.

extra is optional, and its keys are merged into the member's extra at the top level — the same place PATCH puts them. Other body keys that are not member fields are kept in extra too.

Responses: 201 with the member; 409 when the key or user_id is already taken (member_key_taken, user_id_taken), naming the existing key so the caller can switch to PATCH, or when an active member is created with a did another active member holds anywhere in the registry (did_taken, REQ-0096); 409 sensor_held when an active member is created with a meter whose sensor another active member holds, and 409 delivery_point_held with a delivery point, or a meter pod, another active member holds; 404 community_not_found; 422 invalid_role, invalid_status or unknown_area when role, status or area is out of set (the area must be a key of the community's areas).

PATCH /admin/communities/{community_key}/members/{member_key}

Partial update. Absent fields are left alone, never cleared.

delivery_points is deliberately not accepted here — it is a JSONB list, and a patch that happened to omit it would read as "this member now has none". Use the delivery-point routes.

This is how a member's dataspace did is written, because the identity is minted a step after the member is registered. Re-sending a member the DID it already holds is a 200 that changes nothing, so the write is safe to retry.

Responses: 200; 409 user_id_taken if the new user_id belongs to another member of the community, or 409 did_taken if the new did belongs to any other active member in the registry and this member is active after the patch (REQ-0096). A DID clash inside the addressed community names the holding member; one in another community does not, because which member of which other community holds a DID is not the caller's question. status: active on a member that was not active re-checks its sensors and delivery points: 409 sensor_held / 409 delivery_point_held, and nothing in the patch is applied, when another active member holds one.

role and area are still accepted here, with the checks of the profile route below; status too. Out of set: 422 invalid_role, invalid_status, unknown_area, and nothing in the patch is applied. Only the fields a patch names are checked.

PATCH /admin/communities/{community_key}/members/{member_key}/profile

A member's role and area, and nothing else — the route a community dashboard holding only rec-registry.members.profile.write writes through (rec-registry.members.write and .admin reach it too).

{"role": "prosumer", "area": "south"}

At least one of the two, and no other key. Absent fields are left alone; the member's status is not looked at and its assets are left as they are.

Outcome Answer
written 200 with the member (MemberDetail)
an empty body, an unknown key, a null, or any other member field (user_id, did, status, name, type, extra) 422, FastAPI's validation body; nothing changes
role outside consumer, prosumer, producer, operator, admin 422 invalid_role
area not a key of the community's areas 422 unknown_area
unknown community / member 404 community_not_found / member_not_found

PUT /admin/communities/{community_key}/members/{member_key}/name|role|area

One field of a member, and nothing else: {"name": "…"}, {"role": "…"}, {"area": "…"}. Each derives its own action (members.name.write, members.role.write, members.area.write), so a service can be granted one field alone; the supersets are in the table at the top.

Outcome Answer
written 200 with the member (MemberDetail)
the key absent or null, or any other key beside it 422, FastAPI's validation body; nothing changes
role out of set / area not one of the community's 422 invalid_role / unknown_area — the same answer the general PATCH gives
unknown community / member 404 community_not_found / member_not_found

POST /admin/communities/{community_key}/members/{member_key}/status

Move a member through pending → active → suspended → inactive, with an optional reason recorded on the member.

A move to active re-checks the member's sensors and delivery points — only an active member holds one — and answers 409 sensor_held / 409 delivery_point_held, leaving the status unchanged, when another active member took one meanwhile; and 409 did_taken, naming nobody, when another active member holds its did (REQ-0096). An unknown status is 422 invalid_status.

DELETE /admin/communities/{community_key}/members/{member_key}

Deactivates the member (status = inactive). A member who leaves still has metering history, past consents and provenance elsewhere that reference them, and assets cascade on a real delete. The row keeps its user_id and did; it no longer answers /user (REQ-0094) and no longer blocks its DID (REQ-0096).

?purge=true erases the member and its assets permanently. It requires the separate rec-registry.members.purge grant, so a service that manages members day to day cannot perform one.

Response: DeletionReport — purged tells the caller which happened.

PUT|DELETE /admin/communities/{ck}/members/{mk}/delivery-points/{point_id}

Add, replace or remove one supply point, keeping the others. The body id must match the path (422 otherwise). Derives members.delivery_points.write.

One active holder per delivery point across the registry, compared trimmed and case-insensitively: a PUT giving an active member a point another active member holds is 409 delivery_point_held (the holder named by key only inside this community). A member that is not active is checked when it is reactivated.

Correcting a POD is one write: PUT …/delivery-points/{new}?replaces={old} with the new point as body. In one transaction it adds new, removes old and relinks the member's meters whose pod named old to new (as the path spells it); a failure changes nothing.

Outcome Answer
corrected 200, {"delivery_points": [...]} — the member's points after the write
old is not one of this member's points 404 (plain {"detail"})
new is held by another active member 409 delivery_point_held

DELETE of a point one of the member's meters still names is 409 delivery_point_linked: correct it with replaces, or detach the meter, first. An unknown point is 404.

Duplicates already stored are not repaired by the check; an operator lists them with celine-rec-registry duplicate-delivery-points — a read-only CLI report over GET /admin/export (the export grant), one tab-separated line per active holder: delivery_point community member active_holders, the point trimmed and lower-cased; exit 0 none, 1 some, 2 unreadable. One community's share of the same list is GET /admin/communities/{community_key}/delivery-points/duplicates (a read; holders in other communities counted, not named).

PUT|DELETE /admin/communities/{ck}/members/{mk}/assets/{asset_key}

Create, replace or remove one asset. properties is validated against the model for asset_type (pv, storage, meter, ev_charger, heat_pump, load), so an EV charger cannot be stored carrying a heat pump's fields.

Attaching a meter is a PUT at meter-<sensor id>, the id trimmed:

{"key": "meter-SEN-1", "asset_type": "meter",
 "properties": {"name": "Meter", "sensor_id": "SEN-1", "meter_type": "consumption"}}
Outcome Answer
attached 200 with the stored asset (AssetDetail)
already attached to this member 200, nothing changes
another active member, in any community, holds the sensor 409 sensor_held — the holder is named by key only inside this community
the meter's pod is not one of the member's delivery points, and another active member, in any community, holds it as a delivery point or through a meter 409 delivery_point_held — named as for the sensor (REQ-0093)
another member of this community holds the key (with the convention: an inactive member still holding the asset) 409 asset_key_taken
the sensor id is blank after trimming 422
the asset key is longer than 128 characters 422 asset_key_too_long

Sensor ids are compared and stored trimmed (of any Unicode whitespace); only an active member holds one, so a write to a member that is not active is checked when it is reactivated.

Detaching is DELETE on the same path: a hard delete, 204, after which the sensor may be attached elsewhere. An asset the member does not hold is 404 asset_not_found; an unknown member 404 member_not_found.

PATCH /admin/communities/{community_key}

Update community metadata. It does not touch areas or topology, for the same reason the member patch does not touch delivery points: both have their own routes, below.

PUT|DELETE /admin/communities/{community_key}/areas/{area_key}

Add, replace or remove one area; both answer the whole community. Deleting is refused with 409 area_in_use while members still reference it — an orphaned Member.area is a dangling reference nothing else checks. A member write moving somebody into the area at the same moment is counted, not orphaned: both serialise on the community's row.

An area is one GSE primary substation (REQ-0067). The PUT body (AreaUpsert):

{
  "name": "northern",
  "boundary": {"source": "gse_cabine_primarie", "id": "AC000E00000"},
  "topology": ["AC000E00000"]
}

location and geometry are accepted and unused; other keys are dropped. The area is stored and returned with boundary and topology — every read of a community carries both on each area (Area.boundary is null on an area stored before schema v0.7 without one).

Refused 422 invalid_area_boundary, nothing changed
no boundary, null, a list, or not exactly {source, id} of strings coded, not FastAPI's validation body
source other than gse_cabine_primarie, a blank id, or an id over 64 characters
topology of zero or several ids, or one that is not boundary.id (exact)
the node is not in the community's topology, is there twice, or is not a primary_substation write the node first (PUT …/topology/{node_id})
another area of the community carries the same boundary.id two writers at once: one succeeds

The key is an area key: letters, digits, - and _, starting with a letter or digit, at most 128 characters — what a rename accepts as new_key and onboarding's template import holds a template's keys to. Any other area_key is 422 invalid_area_key, judged before the body, and nothing changes. An area stored under another key before the rule is read as stored; the rename, below, moves it onto a key that keeps the rule.

An unknown community is 404 community_not_found, before the key is judged. The PUT judges the area it writes: areas stored before the rule are not re-judged, but the written area may not share a boundary id with them. The registry never checks the code against the GSE dataset.

POST /admin/communities/{community_key}/areas/{area_key}/rename

Move an area to a new key, with its members (REQ-0079); derives community.write. Body {"new_key": "nord"} and nothing else. In one transaction under the community's row lock the area — name, boundary, topology, as stored — is written under new_key, every member of the community whose area is area_key (any status) is moved to it, and area_key is removed. Nothing else changes: not the other areas, the topology, the members' other fields or their assets.

{"old_key": "north", "new_key": "nord", "members_moved": 2, "community": {"key": "example-rec", "areas": {"nord": {}}}}

community is the whole community after the rename (abridged above).

Refused, nothing changed
new_key is not letters, digits, - and _, starting with a letter or digit, at most 128 characters 422 invalid_area_key
the community has no area area_key 404 area_not_found
the community already has new_key (area_key itself included) 409 area_key_taken
an unknown community 404 community_not_found
a body with another key, or none 422, FastAPI's validation body

This is the write for a template sync that renames an area with members: an area PUT under the new key is refused (one area per boundary) and the old key's DELETE is refused while members hold it. A member write naming an area at the same moment serialises with the rename on the community's row.

PUT|DELETE /admin/communities/{community_key}/topology/{node_id}

Add, replace or remove one topology node (REQ-0072); both answer the whole community and derive community.write. The PUT body is the bundle's node (TopologyNodeIn), and its id must match the path (422 otherwise, plain body):

{"id": "AC000E00000", "type": "primary_substation", "name": "Northern primary", "operator_id": "example-dso"}

name, operator_id, parent and area are optional; other keys are dropped. Nodes merge by id: re-sending one replaces it where it stands (the whole node, not field by field), a new one is appended, and every other node is kept.

Refused
PUT changing the type of a node an area lists away from primary_substation 422 invalid_area_boundary, nothing changed; an area stored before the rule is not re-judged
DELETE of a node any area lists 409 topology_node_in_use, naming the areas: change or delete them first
DELETE of a node another node names as its parent 409 topology_node_in_use, naming those nodes by id: re-parent or delete them first
DELETE of a node the community does not have 404, plain body
an unknown community 404 community_not_found

The order a template sync writes in: the node, then the area onto it. Both routes serialise with the area routes on the community's row. No node is left naming a deleted node as its parent; a node naming itself does not hold itself.


Meta Routes

GET /health

Health check. Returns {"status": "ok"}.

GET /version

Service version information.