mero.admin (AdminApi) is the node’s /admin-api REST surface — the full set
(~140 methods) across applications, contexts, namespaces, groups, blobs, aliases,
capabilities, metadata, upgrades, accounts/devices, and TEE. Every method is a suspend fun over
the shared HttpClient ; most reads unwrap the node’s
{ data } envelope for you. Endpoints below are relative to /admin-api.
List methods return the node’s full result set — the admin API does not paginate,
so there are no limit/offset/cursor parameters. The one exception is
listMemberDevices / listGroupMemberDevices, which take optional offset and
limit.
Pinned to core 0.11.0-rc.83
rc.83 removed GET /certificate (so getCertificate is gone), teeVerifyQuote
went with its long-dead route, CreateGroupRequest lost groupId (ids are
derived; naming one is a 400), and an absent subgroup visibility now means
open . Refusals answer with typed statuses — see
Error model .
Method
Endpoint
healthCheck(): HealthStatus
GET /health
isReady(): HealthStatus
GET /ready
isAuthed(): AdminAuthStatus
GET /is-authed
getNodeIdentity(): NodeIdentity
GET /identity
Method
Endpoint
installApplication(request): InstallApplicationResponseData
POST /install-application
installFromRegistry(packageName, version): InstallApplicationResponseData
POST /install-application
getRegistryVersions(registryUrl, packageName): List<String>
off-node registry read
installDevApplication(request): InstallApplicationResponseData
POST /install-dev-application
uninstallApplication(appId): UninstallApplicationResponseData
DELETE /applications/{appId}
listApplications(): ListApplicationsResponseData
GET /applications
getApplication(appId): GetApplicationResponseData
GET /applications/{appId}
getApplicationAbi(appId, serviceName?): JsonElement?
GET /applications/{appId}/abi
listApplicationVersions(applicationId): List<ApplicationVersionEntry>
GET /applications/{applicationId}/versions
Installs are registry-only since core 0.11.0-rc.31
InstallApplicationRequest is {package, version} and nothing else — the node
fetches from the registry it is configured with. The request carries
deny_unknown_fields, so a body still naming url is refused, not ignored:
400 unknown field \url`, expected `package` or `version`. installDevApplicationtakes only apath, and that path must be an .mpkbundle — a raw.wasm` is
refused with “not a signed application bundle” .
Method
Endpoint
listPackages(): ListPackagesResponseData
GET /packages
listPackageVersions(packageName): ListVersionsResponseData
GET /packages/{packageName}/versions
getLatestPackageVersion(packageName): GetLatestVersionResponseData
GET /packages/{packageName}/latest
Method
Endpoint
createContext(request): CreateContextResponseData
POST /contexts
deleteContext(contextId, request? = null): DeleteContextResponseData
DELETE /contexts/{contextId}
getContexts(): GetContextsResponseData
GET /contexts
getContext(contextId): Context
GET /contexts/{contextId}
getContextsForApplication(applicationId): GetContextsResponseData
GET /contexts/for-application/{applicationId}
generateContextIdentity(): GenerateContextIdentityResponseData
POST /identity/context
getContextIdentities(contextId): GetContextIdentitiesResponseData
GET /contexts/{contextId}/identities
getContextIdentitiesOwned(contextId): GetContextIdentitiesResponseData
GET /contexts/{contextId}/identities-owned
joinContext(contextId): JoinContextResponseData
POST /contexts/{contextId}/join
getContextGroup(contextId): ContextGroupResponseData
GET /contexts/{contextId}/group
getContextStorage(contextId): ContextStorageResponseData
GET /contexts/{contextId}/storage
syncContext(contextId? = null)
POST /contexts/sync/{contextId?}
resyncContext(contextId, request = ResyncContextRequest()): ResyncContextResponseData
POST /contexts/{contextId}/resync
performIntent(contextId, request): PerformIntentResponseData
POST /contexts/{contextId}/intents
queryContext(contextId, request): QueryContextResponseData
POST /contexts/{contextId}/query
updateContextApplication(contextId, request)
POST /contexts/{contextId}/application
getContextsWithExecutorsForApplication(applicationId): ContextsWithExecutorsResponseData
GET /contexts/with-executors/for-application/{applicationId}
Method
Endpoint
uploadBlob(request): UploadBlobResponseData
PUT /blobs
deleteBlob(blobId): DeleteBlobResponseData
DELETE /blobs/{blobId}
listBlobs(): ListBlobsResponseData
GET /blobs
getBlob(blobId, contextId? = null): ByteArray
GET /blobs/{blobId}[?context_id=] (raw bytes)
getBlobInfo(blobId, contextId? = null): GetBlobInfoResponseData
HEAD /blobs/{blobId}[?context_id=]
Method
Endpoint
createContextAlias(request): CreateAliasResponseData
POST /alias/create/context
createApplicationAlias(request): CreateAliasResponseData
POST /alias/create/application
lookupContextAlias(name): LookupAliasResponseData
POST /alias/lookup/context/{name}
lookupApplicationAlias(name): LookupAliasResponseData
POST /alias/lookup/application/{name}
deleteContextAlias(name): DeleteAliasResponseData
POST /alias/delete/context/{name}
deleteApplicationAlias(name): DeleteAliasResponseData
POST /alias/delete/application/{name}
listContextAliases(): ListAliasesResponseData
GET /alias/list/context
listApplicationAliases(): ListAliasesResponseData
GET /alias/list/application
createDeviceAlias(request): CreateAliasResponseData
POST /alias/create/device
lookupDeviceAlias(name): LookupAliasResponseData
POST /alias/lookup/device/{name}
deleteDeviceAlias(name): DeleteAliasResponseData
POST /alias/delete/device/{name}
listDeviceAliases(): ListAliasesResponseData
GET /alias/list/device
Three scopes: context, application, device
There is no identity/{contextId} scope — every route under it answers 404 .
The SDK used to offer four methods there; they are gone, replaced by the device
scope core actually serves.
The list routes answer a map — {"data": {"<alias>": "<value>"}}, and
{"data": {}} when empty — so ListAliasesResponseData is a
List<AliasEntry> the SDK builds from it. Modeled as {aliases: […]} it threw
MissingFieldException on every call, empty or not.
Method
Endpoint
listNamespaces(): ListNamespacesResponseData
GET /namespaces
getNamespace(namespaceId): Namespace
GET /namespaces/{namespaceId}
admitJoin(namespaceId, request): AdmitJoinResponseData
POST /namespaces/{namespaceId}/admit
revokeAccountDevice(namespaceId, request): RevokeDeviceResponseData
POST /namespaces/{namespaceId}/account/revoke
listNamespacesForApplication(applicationId): ListNamespacesResponseData
GET /namespaces/for-application/{applicationId}
createNamespace(request): CreateNamespaceResponseData
POST /namespaces
deleteNamespace(namespaceId, request? = null): DeleteNamespaceResponseData
DELETE /namespaces/{namespaceId}
createNamespaceInvitation(namespaceId, request? = null): CreateNamespaceInvitationResult
POST /namespaces/{namespaceId}/invite
joinNamespace(namespaceId, request): JoinNamespaceResponseData
POST /namespaces/{namespaceId}/join
leaveNamespace(namespaceId, request? = null)
POST /namespaces/{namespaceId}/leave
createGroupInNamespace(namespaceId, request? = null): CreateGroupInNamespaceResponseData
POST /namespaces/{namespaceId}/groups
listNamespaceGroups(namespaceId): List<SubgroupEntry>
GET /namespaces/{namespaceId}/groups
changeNamespaceAdmin(namespaceId, request)
POST /namespaces/{namespaceId}/admin (root-guarded)
linkAccountDevice(namespaceId, request): LinkAccountDeviceResponseData
POST /namespaces/{namespaceId}/account/link-device
Subgroups are open unless you ask (core 0.11.0-rc.83)
rc.83 flipped what an absent visibility means on POST /namespaces/{id}/groups:
it used to create a restricted subgroup and now creates an open one. The SDK
never leaves it to the node — createGroupInNamespace always sends a visibility,
filling CreateGroupInNamespaceRequest.VISIBILITY_OPEN when yours is null — so
the same call creates the same subgroup on any node. Pass VISIBILITY_RESTRICTED
for a membership boundary.
Method
Endpoint
createGroup(request: CreateGroupRequest): CreateGroupResponseData
POST /groups (also a Map overload; never send groupId)
getGroupInfo(groupId): GroupInfoResponseData
GET /groups/{groupId}
deleteGroup(groupId, request? = null): DeleteGroupResponseData
DELETE /groups/{groupId}
listGroupMembers(groupId): ListGroupMembersResponseData
GET /groups/{groupId}/members
listGroupContexts(groupId): ListGroupContextsResponseData
GET /groups/{groupId}/contexts
addGroupMembers(groupId, request)
POST /groups/{groupId}/members
removeGroupMembers(groupId, request)
POST /groups/{groupId}/members/remove
updateMemberRole(groupId, identity, request)
PUT /groups/{groupId}/members/{identity}/role
setMemberAutoFollow(groupId, identity, request)
PUT /groups/{groupId}/members/{identity}/auto-follow
listSubgroups(groupId): List<SubgroupEntry>
GET /groups/{groupId}/subgroups
reparentGroup(childGroupId, request): ReparentGroupResponseData
POST /groups/{childGroupId}/reparent
detachContextFromGroup(groupId, contextId, request? = null)
POST /groups/{groupId}/contexts/{contextId}/remove
leaveGroup(groupId, request? = null)
POST /groups/{groupId}/leave
leaveContext(contextId, request? = null)
POST /contexts/{contextId}/leave
transferOwnership(groupId, request)
POST /groups/{groupId}/transfer-ownership (root-guarded)
ownerDeleteGroup(groupId, request = OwnerDeleteGroupRequest())
POST /groups/{groupId}/owner-delete (root-guarded)
sealToAccount(groupId, account, request): SealedEnvelope
POST /groups/{groupId}/accounts/{account}/seal
transferOwnership, changeNamespaceAdmin, ownerDeleteGroup,
setTeeAdmissionPolicy and the TEE authoring policy are owner operations:
they need a proof signed by the owner account’s root key, not just a device.
Pass it as rootProof (hex borsh SignedOwnerOp), or call a node that holds the
owner’s root and let it sign. With neither, 403.
The proof names the group’s namespaceId and ownerOpCounter — read both from
getGroupInfo first. A stale counter is a 409 : re-read and re-sign. An
empty rootProof is a 400; omit the field instead. ownerDeleteGroup needs a
group with no contexts; deleteGroup stays the admin-level cascading delete.
Method
Endpoint
createGroupInvitation(groupId, request? = null): CreateGroupInvitationResult
POST /groups/{groupId}/invite
joinGroup(request): JoinGroupResponseData
POST /groups/join
joinSubgroupInheritance(groupId): JoinSubgroupInheritanceResponseData
POST /groups/{groupId}/join-via-inheritance
issueOwnershipProof(groupId, request: IssueOwnershipProofRequest): IssueOwnershipProofResponseData
POST /groups/{groupId}/issue-ownership-proof
issueNamespaceOwnershipProof(groupId, request: IssueNamespaceOwnershipProofRequest): IssueOwnershipProofResponseData
POST /groups/{groupId}/issue-namespace-ownership-proof (adds founding, credential)
issueOwnershipProof(groupId, request: Map? = null): JsonElement
untyped overload
issueNamespaceOwnershipProof(groupId, request: Map? = null): JsonElement
untyped overload
Method
Endpoint
getMemberCapabilities(groupId, identity): MemberCapabilities
GET /groups/{groupId}/members/{identity}/capabilities
setMemberCapabilities(groupId, identity, request)
PUT /groups/{groupId}/members/{identity}/capabilities
getDefaultCapabilities(groupId): Int
(reads getGroupInfo)
setDefaultCapabilities(groupId, request)
PUT /groups/{groupId}/settings/default-capabilities
getSubgroupVisibility(groupId): String
(reads getGroupInfo)
setSubgroupVisibility(groupId, request)
PUT /groups/{groupId}/settings/subgroup-visibility
getTeeAdmissionPolicy(groupId): GetTeeAdmissionPolicyResponseData
GET /groups/{groupId}/settings/tee-admission-policy
setTeeAdmissionPolicy(groupId, request)
PUT /groups/{groupId}/settings/tee-admission-policy (root-guarded)
setTeeAuthoringPolicy(groupId, request)
PUT /groups/{groupId}/settings/tee-authoring-policy (root-guarded)
disableTeeAuthoringPolicy(groupId, request = …)
DELETE /groups/{groupId}/settings/tee-authoring-policy
openToDelegatedExecution(groupId): CapabilityChange
read-modify-write of the default mask
grantAuthorship(groupId, account): CapabilityChange
read-modify-write of one member’s mask
Two TEE admission forms (core 0.11.0-rc.83)
Build the request with SetTeeAdmissionPolicyRequest.measurements(…) or
.signedRelease(…):
Measurements — allowedMrtd, allowedRtmr1, allowedRtmr2 and
allowedRtmr3 must all be non-empty, even with acceptMock (a mock fleet
names the all-zero measurement). An empty list is a 400, not a wildcard.
Signed release — admit any node release the mero-tee workflow signed, for
the named image allowedProfiles (and an optional minReleaseVersion). Every
measurement list must then be empty.
Both take mode (TeeAdmissionMode.REPLICA, the default, or RELAY, which
admits TEEs as RelayTee — able to author members’ writes under their warrants)
and rootProof. The GET adds enabled, signedRelease and mode.
Method
Endpoint
setGroupMetadata(groupId, request)
PUT /groups/{groupId}/metadata
getGroupMetadata(groupId): MetadataRecord?
GET /groups/{groupId}/metadata
setMemberMetadata(groupId, identity, request)
PUT /groups/{groupId}/members/{identity}/metadata
getMemberMetadata(groupId, identity): MetadataRecord?
GET /groups/{groupId}/members/{identity}/metadata
setContextMetadata(groupId, contextId, request)
PUT /groups/{groupId}/contexts/{contextId}/metadata
getContextMetadata(groupId, contextId): MetadataRecord?
GET /groups/{groupId}/contexts/{contextId}/metadata
Method
Endpoint
syncGroup(groupId, request? = null): SyncGroupResponseData
POST /groups/{groupId}/sync
syncGroupContexts(groupId): ListGroupContextsResponseData
composite — see below
listMemberDevices(groupId, offset? = null, limit? = null): MemberDevices
GET /groups/{groupId}/member-devices[?offset&limit]
listGroupMemberDevices(groupId, offset? = null, limit? = null): List<MemberDeviceGroup>
same, unwrapped
upgradeGroup(groupId, request): UpgradeGroupResponseData
POST /groups/{groupId}/upgrade
getGroupUpgradeStatus(groupId): GroupUpgradeStatusResponseData
GET /groups/{groupId}/upgrade/status
retryGroupUpgrade(groupId, request? = null): RetryGroupUpgradeResponseData
POST /groups/{groupId}/upgrade/retry
getMigrationStatus(namespaceId): MigrationStatus
GET /groups/{namespaceId}/migration-status
getCascadeStatus(namespaceId): List<CascadeStatusEntry>
GET /groups/{namespaceId}/cascade-status
abortMigration(namespaceId, request? = null): JsonElement
POST /groups/{namespaceId}/migration/abort
Joining a group does not initialize its contexts on your node: cross-node sync is
asynchronous, and until a context is joined and state-pulled it sits on the
all-ones (1111…) uninitialized hash, so every read looks empty.
syncGroupContexts does the whole dance for you:
// sync the group, then join + state-pull every context in it
val contexts = mero.admin. syncGroupContexts (groupId)
It runs syncGroup, lists the group’s contexts, then joinContext +
syncContext for each, tolerating per-context failures (sync is async — call it
again). The sample app’s chat calls it in a short retry loop right after
joinNamespace, which is what makes an invited space’s channels appear.
A device pairs to an account once (core 0.11.0-rc.28 moved this off the
namespace), then links into namespaces. Revocation stayed per-namespace, because
that is where the group key rotates.
Method
Endpoint
listAccountDevices(): AccountDevices
GET /account/devices
listAccountApplications(): AccountApplications
GET /account/applications
pairInit(request): PairInitResponseData
POST /account/pair-init
pairComplete(request): PairCompleteResponseData
POST /account/pair-complete
relinkDevice(deviceId, request): RelinkDeviceResponseData
POST /account/devices/{deviceId}/relink
rescopeDevice(deviceId, request): RescopeDeviceResponseData
PUT /account/devices/{deviceId}/scope
labelDevice(deviceId, request): LabelDeviceResponseData
PUT /account/devices/{deviceId}/label
signWithAccountRoot(request): AccountSignWithRootResponseData
POST /account/sign-with-root
`relink` adds, `scope` replaces (core 0.11.0-rc.41)
relinkDevice is add-only, so it can widen a device’s reach but never narrow it.
rescopeDevice is the other direction, and a PUT for that reason: it replaces
the scope outright.
The body is {"scope":"all"} or {"scope":{"only":["<application id>", …]}} —
serde’s externally-tagged enum, so DeviceScope.All is the bare string
"all", not an object. Tagged rather than a list whose emptiness means
everything (which is what relink uses): here an empty only is refused with a
400, instead of the narrowest slip becoming the widest possible grant.
Both routes must run on the node holding the account root — only its key can
sign the change. The device is not consulted and need not be online.
labelDevice names a device so a listing renders as something other than 64 hex
characters. The name replicates: it comes back on AccountDevice.label for every
device of the account, and labelEpoch orders it against a rename made elsewhere
at the same moment.
Three of these answer flat, not enveloped
GET /account/devices, GET /account/applications and
GET /groups/{id}/member-devices return {devices:[…]} / {applications:[…]} /
{members:[…]} with no data wrapper , unlike every route around them.
Decoding one as an envelope yields null and reads as “empty”.
Pairing statements expire (core 0.11.0-rc.83)
The pairing statement is now 72 bytes (144 hex) and carries a timestamp:
pair-complete refuses one older than 300 s. The SDK relays it opaquely.
A member who cannot run the application — a device holding only a signing key —
asks a node to act for it under a signed warrant . These calls take warrants
and proofs as opaque hex; minting them is the account layer’s job. On a node
running with delegated_access, the routes are public (the warrant is the
credential).
Method
Endpoint
getIntentRelay(contextId): IntentRelayInfo
GET /contexts/{contextId}/intents
createContextIntent(groupId, request): CreateContextIntentResponseData
POST /groups/{groupId}/context-intents
getContextIntentRelay(groupId, author? = null): ContextIntentRelayInfo
GET /groups/{groupId}/context-intents[?author=]
governanceIntent(groupId, request): GovernanceIntentResponseData
POST /groups/{groupId}/governance-intents
getGovernanceIntentRelay(groupId): GovernanceIntentRelayInfo
GET /groups/{groupId}/governance-intents
presenceIntent(contextId, request)
POST /contexts/{contextId}/presence-intents (204)
getWarrantNonce(contextId, authorDeviceKey): WarrantNonceState
GET /contexts/{contextId}/warrant-nonce/{key}
getWarrantNonceAsAuthor(contextId, authorProof): WarrantNonceState
POST /contexts/{contextId}/warrant-nonce
Read getIntentRelay before signing: a warrant must name exactly its
executorAccount, executorKey, releaseBytecodeId and releaseVersion, and a
warrant naming anything else spends a nonce and is refused. canAuthorOnBehalf: false is an answer, not an error — an admin of groupId grants
Capabilities.CAN_AUTHOR_ON_BEHALF (openToDelegatedExecution /
grantAuthorship).
Warrant-nonce routes are newer than rc.83
A node without them answers 404, which the SDK throws as
WarrantNonceRouteUnavailableException — fall back to your own counter. Nonces
are ULong, parsed from the literal digits, never through a Double.
Method
Endpoint
getTeeInfo(): TeeInfoResponseData
GET /tee/info
teeAttest(request): TeeAttestResponseData
POST /tee/attest
teeRegistrationAttest(request): TeeAttestResponseData
POST /tee/registration-attest
TeeAttestRequest takes optional bindNodeKey, bindTransportKey and
includeCollateral; the response adds boundPublicKey, transportPublicKey
and collateral. teeVerifyQuote is gone — its route was removed from core
before rc.41.
Method
Endpoint
getPeersCount(): PeersCountResponseData
GET /peers
getNetworkStatus(): JsonElement
GET /network/status
getUsage(): JsonElement
GET /usage
The file also exports a top-level helper compareSemver(a: String, b: String): Int
for ordering dotted version strings.
val applicationId: String ,
val serviceName: String ? = null ,
val contextStateHash: String ,
val dagHeads: List < List < Int >>,
val applicationVersion: String ? = null ,
// The element type of getContexts() / getContextsForApplication().
data class ContextWithGroup ( /* Context fields … */ val groupId: String ? = null )
val namespaceId: String , val appKey: String , val targetApplicationId: String ,
val createdAt: Long , val name: String ? = null ,
val memberCount: Int , val contextCount: Int , val subgroupCount: Int ,
val appVersion: String ? = null ,
val founding: NamespaceFounding ? = null , // rc.83: founderAccountId + salt
val heldOps: NamespaceHeldOps ? = null , // rc.83: ops held for a missing key
val groupId: String , val appKey: String , val targetApplicationId: String ,
val memberCount: Int , val contextCount: Int ,
val activeUpgrade: GroupUpgradeStatus ? = null ,
val defaultCapabilities: Int , val subgroupVisibility: String ,
val metadata: MetadataRecord ? = null , val groupStateHash: String ? = null ,
val namespaceId: String ? = null , val ownerOpCounter: Long ? = null , // rc.83
data class BlobInfo ( val blobId: String , val size: Long ) // GetBlobInfoResponseData adds hash?, mimeType?
data class MetadataRecord (
val name: String ? = null , val data : Map < String , String >,
val updatedAt: Long , val updatedBy: String , // MILLIS, and u64 in core
val accountId: String , val deviceId: String ? = null , val publicKey: String ,
val accountRootPublicKey: String , val deviceAgreementKey: String ? = null ,
val holdsAccountRoot: Boolean ? = null ,
data class MemberCapabilities ( val capabilities: Int ) // see /reference/capabilities/
upgradePolicy is gone
core#3485 removed the concept — it always held "LazyOnAccess" — so neither
Namespace nor GroupInfo carries it, and neither CreateNamespaceRequest nor
CreateGroupRequest sends it. A model that still declares it required throws
MissingFieldException on every read; ignoreUnknownKeys does not help, because
the problem is a missing field, not an unknown one.
Timestamps are Long, not Int: MetadataRecord.updatedAt is wall-clock
milliseconds and overflows a 32-bit Int today.
See AdminTypes.kt
for the complete set of request and response types.