API
Magistrala API reference for users, workspaces, devices, channels, groups, and messages — Atom's GraphQL API plus the still-current HTTP/MQTT/CoAP/WS message publishing endpoints, with sample requests.
Reference
The API reference is available at the Magistrala API documentation site
The Workspaces, Devices, Channels, Groups, and Users sections below cover Atom's GraphQL API (POST /graphql). Messages is a different story: publishing over HTTP/MQTT/CoAP/WS is unrelated to Atom and uses its own REST-style endpoints, documented in its own section below.
Users
A User is an Atom Entity of kind human — the same generic entity type documented in Devices, with the same createEntity/entity/entities/updateEntity/enableEntity/disableEntity/deleteEntity mutations and queries. This section only covers what's specific to human users: signing up, logging in, and password/session management (src/graphql/auth.rs, src/graphql/credentials.rs).
No CLI equivalent
Unlike Workspaces/Devices/Channels/Groups, user management has no CLI wrapper — the shipped CLI
(cli/root.go) has no users command. Everything below is GraphQL-only, or reachable through
magistrala-ui's sign-up/login pages.
Same connection details as the rest of this API reference: POST http://localhost:8080/graphql, Content-Type: application/json, body {"query": "...", "variables": {...}}. Login/signup requests need no Authorization header (they establish one); everything else needs Authorization: Bearer <user_token>.
Sign Up
Self-service registration — creates a new human entity and, depending on configuration, sends a verification email before the account is usable.
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"query": "mutation Signup(\$input: SignupInput!) { signup(input: \$input) { entityId email verificationRequired } }",
"variables": {
"input": {
"name": "<full_name>",
"email": "<email>",
"password": "<password>",
"attributes": {}
}
}
}
EOFExpected response:
{
"data": {
"signup": {
"entityId": "b3f1c9a2-5e2d-4a11-9c3f-7a8b2e6d1f90",
"email": "user@example.com",
"verificationRequired": true
}
}
}Notes:
- Signup only succeeds if
self_registration_enabledis set on the deployment; otherwise it returns"sign up is not enabled". Admin-created accounts (self-registration disabled) go throughcreateEntityinstead — see Create User below — but that path doesn't set a password or send verification email; a separate Set Password call is needed to make the account loginable.Entitytype itself.
Log In
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"query": "mutation Login(\$input: LoginInput!) { login(input: \$input) { token entityId sessionId } }",
"variables": {
"input": {
"identifier": "<email>",
"secret": "<password>",
"kind": "password"
}
}
}
EOFExpected response:
{
"data": {
"login": {
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
"entityId": "b3f1c9a2-5e2d-4a11-9c3f-7a8b2e6d1f90",
"sessionId": "9c7a1e2d-3b4f-4a5e-8d6c-1f2a3b4c5d6e"
}
}
}Notes:
kinddefaults to"password"if omitted;"shared_key"is also accepted (src/graphql/auth.rs'sparse_login_credential_kind) for shared-key-based login.- To log into a specific workspace directly, pass
tenantIdortenantAlias(the workspace'sroute) alongsideidentifier/secret— sameLoginInput.- The returned
tokenis the bearer token for every other request in this reference.
Log Out / Refresh Session
# Log out (revokes the current session)
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d '{"query": "mutation { logout }"}'
# Refresh (issues a new token for the current session)
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d '{"query": "mutation { refreshSession { token entityId sessionId } }"}'refreshSession requires the token to actually carry a session (i.e. it came from login, not a personal access token) — it errors with "session refresh requires a session token" otherwise.
Create User
Admin-created account — bypasses self-registration, does not set a password. Same createEntity mutation as Devices, with kind: "human".
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation CreateUser(\$input: CreateEntityInput!) { createEntity(input: \$input) { id kind name tenantId status attributes createdAt } }",
"variables": {
"input": {
"kind": "human",
"name": "<full_name>",
"tenantId": "<workspace_id>",
"attributes": {}
}
}
}
EOFFollow up with Set Password to make the account loginable, since createEntity has no email/password fields (those are signup-specific — see the note under Sign Up).
Get User / List Users
Same entity(id) and entities(...) queries documented in Get Device / Get Devices, filtered to kind: "human". entities also takes a free-text q argument, which doubles as user search:
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "query ListUsers(\$q: String, \$tenantId: ID) { entities(kind: human, q: \$q, tenantId: \$tenantId, limit: 20) { total items { id name status attributes createdAt } } }",
"variables": { "q": "<search_term>", "tenantId": "<workspace_id>" }
}
EOFUpdate / Enable / Disable / Delete User
Same updateEntity, enableEntity, disableEntity, deleteEntity (soft-delete), restoreEntity, and purgeEntity mutations as Devices — pass the user's entity id. updateEntity can change name and attributes; it cannot change email (see the Sign Up note) or password (see below).
Set / Change Password
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation SetPassword(\$entityId: ID!, \$password: String!) { createPassword(entityId: \$entityId, password: \$password) }",
"variables": { "entityId": "<user_entity_id>", "password": "<new_password>" }
}
EOFReturns true on success. Despite the name, createPassword also handles changing an existing password — there is no separate "update password" mutation. A caller can set another entity's password only with manage capability on it (or on the target's workspace) — see require_credential_management in src/graphql/auth.rs; a scoped access token can never call this at all (self-escalation is explicitly blocked).
Personal Access Tokens
Covered in depth in the Personal Access Tokens user guide and backed by the same credentials.rs mutations (createAccessToken, replaceAccessTokenPermissions, revokeAccessToken) and the accessTokens/credentials queries — not duplicated here.
Workspaces
A Workspace is Atom's Tenant — an isolated organizational unit that contains entities such as Devices, Gateways, Device Types, Channels, Groups, Roles, and Invitations. Every user can belong to one or more workspaces, each identified by a unique route.
Workspaces are managed through Atom's GraphQL API, not a REST service — there is no standalone domains/workspaces HTTP service anymore. All requests below are POST requests to the GraphQL endpoint with an Authorization: Bearer <token> header, Content-Type: application/json, and a JSON body of the shape {"query": "...", "variables": {...}}.
GraphQL endpoint:
http://localhost:8080/graphql(Atom's default port; the base URL is configured via theATOM_URLenvironment variable — see Getting Started)
Create Workspace
Registers a new workspace. Requires the manage or create capability at platform scope.
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation CreateWorkspace(\$input: CreateTenantInput!) { createTenant(input: \$input) { id name alias status tags attributes createdAt updatedAt } }",
"variables": {
"input": {
"name": "<workspace_name>",
"alias": "<workspace_route>",
"tags": ["<tag1>", "<tag2>"],
"attributes": { "region": "EU" }
}
}
}
EOFExpected response:
{
"data": {
"createTenant": {
"id": "d7f9b3b8-4f7e-4f44-8d47-1a6e5e6f7a2b",
"name": "Magistrala",
"alias": "magistrala",
"status": "active",
"tags": ["absmach", "IoT"],
"attributes": { "region": "EU" },
"createdAt": "2026-08-24T14:12:01Z",
"updatedAt": null
}
}
}Notes:
nameis required;alias(the route/slug used to address the tenant, successor to the old Domainalias) is optional but must be unique when set.attributesis a free-form JSON object (successor to the old flatmetadatafield) — see Entities' Attributes / Metadata.- Creating a workspace also seeds its default Device Types.
Get Workspace
Retrieves a specific workspace by ID. Requires read or manage access on the tenant.
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "query Workspace(\$id: ID!) { tenant(id: \$id) { id name alias status tags attributes createdAt updatedAt } }",
"variables": { "id": "d7f9b3b8-4f7e-4f44-8d47-1a6e5e6f7a2b" }
}
EOFGet All Workspaces
Retrieves a paginated list of workspaces. A platform admin sees every workspace; a normal user sees only the workspaces they belong to.
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "query ListWorkspaces(\$q: String, \$name: String, \$alias: String, \$status: TenantStatus, \$limit: Int, \$offset: Int) { tenants(q: \$q, name: \$name, alias: \$alias, status: \$status, limit: \$limit, offset: \$offset) { total items { id name alias status tags attributes createdAt updatedAt } } }",
"variables": { "limit": 20, "offset": 0, "status": "active" }
}
EOFSupported filter/sort arguments:
q(free text),name,alias,status(active/inactive/frozen/deleted),deleted(live/deleted/all),order(created_at/updated_at/name/alias/status),dir(asc/desc),limit,offset.
Update Workspace
Updates the name, alias, tags, or attributes of a workspace.
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation UpdateWorkspace(\$id: ID!, \$input: UpdateTenantInput!) { updateTenant(id: \$id, input: \$input) { id name alias status tags attributes updatedAt } }",
"variables": {
"id": "d7f9b3b8-4f7e-4f44-8d47-1a6e5e6f7a2b",
"input": {
"name": "Magistrala Cloud",
"tags": ["absmach", "cloud"],
"attributes": { "region": "EU", "tier": "premium" }
}
}
}
EOFEnable / Disable / Freeze Workspace
enableTenant, disableTenant, and freezeTenant all take just an id and return the updated Workspace. Freezing locks all entity operations within the workspace while keeping its data readable.
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation EnableWorkspace(\$id: ID!) { enableTenant(id: \$id) { id status } }",
"variables": { "id": "d7f9b3b8-4f7e-4f44-8d47-1a6e5e6f7a2b" }
}
EOFReplace enableTenant with disableTenant or freezeTenant for the other two operations. Platform-admin only (manage at platform scope).
Delete Workspace
deleteTenant(id: ID!): Boolean soft-deletes a workspace. Two further platform-admin-only mutations exist for the deleted state: restoreTenant(id: ID!): Tenant (reactivate within the retention window — revoked sessions/certificates are not reinstated, members must re-authenticate) and purgeTenant(id: ID!): Boolean (irreversible, bypasses the retention window).
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation DeleteWorkspace(\$id: ID!) { deleteTenant(id: \$id) }",
"variables": { "id": "d7f9b3b8-4f7e-4f44-8d47-1a6e5e6f7a2b" }
}
EOFWorkspace Members and Invitations
tenantMembers(tenantId, q, limit, offset): EntityList— list a workspace's members.tenantAssignableEntities(tenantId, q, limit, offset): EntityList— search entities that could be invited/added (requiresqof at least 3 characters).addTenantMember(tenantId, entityId, roleId): Boolean/removeTenantMember(tenantId, entityId): Boolean— direct add/remove, no invitation flow.myTenantRoles(tenantId): [TenantRoleAssignment]— the caller's own roles within a workspace (roleId,roleName,actions,assignmentPaths).createTenantInvitation(tenantId, input: CreateTenantInvitationInput!): TenantInvitation— invite byinviteeUserIdorinviteeEmail, optionally with aroleId; sends an email wheninviteeEmailis set.tenantInvitations(tenantId, limit, offset): TenantInvitationList/myTenantInvitations(limit, offset): TenantInvitationList— list a workspace's pending invitations, or the caller's own.acceptTenantInvitation(tenantId): Boolean,acceptTenantInvitationToken(input: InvitationTokenInput!): ID,rejectTenantInvitation(tenantId): Boolean,revokeTenantInvitation(tenantId, invitationId): Boolean.
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation InviteMember(\$tenantId: ID!, \$input: CreateTenantInvitationInput!) { createTenantInvitation(tenantId: \$tenantId, input: \$input) { id inviteeEmail roleId createdAt } }",
"variables": {
"tenantId": "d7f9b3b8-4f7e-4f44-8d47-1a6e5e6f7a2b",
"input": { "inviteeEmail": "jane@example.com" }
}
}
EOFTip: Upon creation, the workspace creator automatically receives the Admin role within that workspace, granting full access to manage roles, members, devices, channels, and other entities.
Devices
A Device is an Atom Entity of kind device — the same generic entity type used for Gateways and Device Types' bindings (a Gateway is just a Device with attributes.is_gateway set; see Gateway Management). Users and other principals are entities too, of kind human — see Users.
Like Workspaces, Devices are managed through Atom's GraphQL API: POST http://localhost:8080/graphql, Authorization: Bearer <token>, Content-Type: application/json, body {"query": "...", "variables": {...}}. Field/argument names below match Atom's schema (src/graphql/entities.rs, src/graphql/types/mod.rs) and the Devices CLI, which uses the friendlier aliases device_type_id/device_type_version_id for GraphQL's profileId/profileVersionId.
Create Device
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation CreateDevice(\$input: CreateEntityInput!) { createEntity(input: \$input) { id kind name externalId tenantId profileId profileVersionId status attributes createdAt updatedAt } }",
"variables": {
"input": {
"kind": "device",
"name": "<device_name>",
"tenantId": "<workspace_id>",
"externalId": "<serial_or_mac>",
"attributes": { "location": "warehouse-1" }
}
}
}
EOFExpected response:
{
"data": {
"createEntity": {
"id": "b3f1c9a2-5e2d-4a11-9c3f-7a8b2e6d1f90",
"kind": "device",
"name": "Temperature Sensor",
"externalId": "SN-00123",
"tenantId": "d7f9b3b8-4f7e-4f44-8d47-1a6e5e6f7a2b",
"profileId": null,
"profileVersionId": null,
"status": "active",
"attributes": { "location": "warehouse-1" },
"createdAt": "2026-08-24T14:20:01Z",
"updatedAt": null
}
}
}Notes:
externalId(identifier assigned outside Atom — serial number, MAC, SKU) is opaque, case-sensitive, trimmed, and must be unique per workspace among live entities. It replaces the old Client "external ID" concept 1:1.- To create a Gateway, set
attributes: { "is_gateway": true }— there is no separate Gateway create mutation.- To bind a Device Type, pass
profileId(and optionallyprofileVersionId); see Device Type Management.- There is no bulk/batch create mutation in Atom's GraphQL API. Each device must be created with its own
createEntitycall — the old REST API's bulk-create endpoints have no current equivalent.
Get Device
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "query Device(\$id: ID!) { entity(id: \$id) { id kind name externalId tenantId profileId profileVersionId status attributes objectGroupIds createdAt updatedAt } }",
"variables": { "id": "b3f1c9a2-5e2d-4a11-9c3f-7a8b2e6d1f90" }
}
EOFGet Devices
Lists entities, filterable by kind, workspace, text search, attributes, parent group, and more.
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "query ListDevices(\$tenantId: ID, \$kind: EntityKind, \$q: String, \$status: EntityStatus, \$limit: Int, \$offset: Int) { entities(tenantId: \$tenantId, kind: \$kind, q: \$q, status: \$status, limit: \$limit, offset: \$offset) { total items { id kind name externalId status attributes createdAt updatedAt } } }",
"variables": {
"tenantId": "d7f9b3b8-4f7e-4f44-8d47-1a6e5e6f7a2b",
"kind": "device",
"status": "active",
"limit": 20,
"offset": 0
}
}
EOFAdditional filter arguments:
externalId,profileId,attributesContains(JSON containment match),parentGroupId+includeDescendants,deleted(live/deleted/all),order(created_at/updated_at/name/kind/status),dir(asc/desc). To list Gateways specifically, filter client-side onattributes.is_gateway— there is no server-sideisGatewayfilter argument.
Update Device
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation UpdateDevice(\$id: ID!, \$input: UpdateEntityInput!) { updateEntity(id: \$id, input: \$input) { id name status attributes updatedAt } }",
"variables": {
"id": "b3f1c9a2-5e2d-4a11-9c3f-7a8b2e6d1f90",
"input": {
"name": "Temperature Sensor (North Wing)",
"attributes": { "location": "warehouse-1", "floor": 2 }
}
}
}
EOF
attributesinUpdateEntityInputreplaces the whole attributes object, it does not merge — send the full desired object, not just the changed keys.aliasandexternalIduseMaybeUndefinedsemantics: omit the field to leave it unchanged, or passnullexplicitly to clear it.
Create a Device Credential (Secret)
Devices authenticate over MQTT/HTTP/CoAP with a shared key, created via a separate credentials mutation rather than being part of the entity itself (successor to the old Client secret field):
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation CreateDeviceKey(\$entityId: ID!, \$input: CreateSharedKeyInput!) { createSharedKey(entityId: \$entityId, input: \$input) { credentialId key expiresAt } }",
"variables": {
"entityId": "b3f1c9a2-5e2d-4a11-9c3f-7a8b2e6d1f90",
"input": { "description": "primary device key" }
}
}
EOFThe returned
keyis shown once at creation time.revokeCredential(credentialId)revokes it;credentials(entityId)lists a device's credentials (metadata only, keys are not re-displayed).
Enable / Disable Device
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation EnableDevice(\$id: ID!) { enableEntity(id: \$id) { id status } }",
"variables": { "id": "b3f1c9a2-5e2d-4a11-9c3f-7a8b2e6d1f90" }
}
EOFReplace enableEntity with disableEntity to disable. There is a third status, suspended, reachable only via updateEntity's status input — no dedicated suspendEntity mutation exists.
Delete Device
deleteEntity(id: ID!): Boolean soft-deletes. Platform-admin-only restoreEntity(id: ID!): Boolean and purgeEntity(id: ID!): Boolean mirror the Workspace lifecycle above — restoring does not reinstate revoked credentials/sessions.
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation DeleteDevice(\$id: ID!) { deleteEntity(id: \$id) }",
"variables": { "id": "b3f1c9a2-5e2d-4a11-9c3f-7a8b2e6d1f90" }
}
EOFOwnership
addOwnership(ownerId, ownedId, relation) / removeOwnership(ownerId, ownedId) record a direct owner→owned relationship between two entities (e.g. a user owning a device) — this is separate from workspace membership or group assignment.
Messages
Send Messages
Sends message via HTTP protocol
curl -sSiX POST http://localhost/http/m/{workspace_id}/c/{channel_id} \
-H "Content-Type: application/senml+json" \
-H "Authorization: Client <client_secret>" \
-d @- <<EOF
[
{
"bn": "<base_name>",
"bt": <base_time>,
"bu": "<base_unit>",
"bver": <base_version>,
"n": "<measurement_name>",
"u": "<measurement_unit>",
"v": <measurement_value>
},
{
"n": "<measurement_name>",
"t": <measurement_time>,
"v": <measurement_value>
}
]
EOFFor example:
curl -sSiX POST http://localhost/http/m/{workspace_id}/c/aecf0902-816d-4e38-a5b3-a1ad9a7cf9e8 \
-H "Content-Type: application/senml+json" \
-H "Authorization: Client a83b9afb-9022-4f9e-ba3d-4354a08c273a" \
-d @- <<EOF
[
{
"bn": "some-base-name:",
"bt": 1.276020076001e+09,
"bu": "A",
"bver": 5,
"n": "voltage",
"u": "V",
"v": 120.1
},
{
"n": "current",
"t": -5,
"v": 1.2
},
{
"n": "current",
"t": -4,
"v": 1.3
}
]
EOF
HTTP/1.1 202 Accepted
Server: nginx/1.23.3
Date: Thu, 15 Jun 2023 09:40:44 GMT
Content-Length: 0
Connection: keep-aliveRead Messages
Reads messages from database for a given channel, via the timescale-reader/postgres-reader HTTP API (see Storage Architecture § Message Storage Backends).
curl -sSiX GET http://localhost:{service_port}/{workspace_id}/channels/{channel_id}/messages?[offset={offset}]&[limit={limit}] \
-H "Authorization: Bearer <access_token>"For example
curl -sSiX GET http://localhost:9009/6a45444c-4c89-46f9-a284-9e731674726a/channels/aecf0902-816d-4e38-a5b3-a1ad9a7cf9e8/messages \
-H "Authorization: Client a83b9afb-9022-4f9e-ba3d-4354a08c273a"
HTTP/1.1 200 OK
Content-Type: application/json
Date: Wed, 05 Apr 2023 16:01:49 GMT
Content-Length: 660
{
"offset": 0,
"limit": 10,
"format": "messages",
"total": 3,
"messages": [
{
"channel": "aecf0902-816d-4e38-a5b3-a1ad9a7cf9e8",
"publisher": "2766ae94-9a08-4418-82ce-3b91cf2ccd3e",
"protocol": "http",
"name": "some-base-name:voltage",
"unit": "V",
"time": 1276020076.001,
"value": 120.1
},
{
"channel": "aecf0902-816d-4e38-a5b3-a1ad9a7cf9e8",
"publisher": "2766ae94-9a08-4418-82ce-3b91cf2ccd3e",
"protocol": "http",
"name": "some-base-name:current",
"unit": "A",
"time": 1276020072.001,
"value": 1.3
},
{
"channel": "aecf0902-816d-4e38-a5b3-a1ad9a7cf9e8",
"publisher": "2766ae94-9a08-4418-82ce-3b91cf2ccd3e",
"protocol": "http",
"name": "some-base-name:current",
"unit": "A",
"time": 1276020071.001,
"value": 1.2
}
]
}Note: The
<service_port>depends on the active reader service you're using. The example above uses the HTTP interface of the Postgres Reader.
- Use
9009for the Postgres Reader (HTTP)- Use
9011for the Timescale Reader (HTTP)
Channels
A Channel is an Atom Resource of kind channel — messaging endpoints that Devices publish to and subscribe through (see Messages below for the actual publish/subscribe protocol traffic, which is unrelated to Atom).
Like Workspaces and Devices, Channels are managed through Atom's GraphQL API: POST http://localhost:8080/graphql, Authorization: Bearer <token>, Content-Type: application/json, body {"query": "...", "variables": {...}}. Field/argument names below match Atom's schema (src/graphql/resources.rs, src/graphql/types/mod.rs) and the Channels CLI.
Create Channel
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation CreateChannel(\$input: CreateResourceInput!) { createResource(input: \$input) { id kind name alias tenantId ownerId attributes createdAt updatedAt } }",
"variables": {
"input": {
"kind": "channel",
"name": "<channel_name>",
"tenantId": "<workspace_id>",
"attributes": { "protocol": "mqtt" }
}
}
}
EOFExpected response:
{
"data": {
"createResource": {
"id": "9c2e7a41-3b5d-4f88-a2e0-6d1c8f4e9b73",
"kind": "channel",
"name": "Telemetry Channel",
"alias": null,
"tenantId": "d7f9b3b8-4f7e-4f44-8d47-1a6e5e6f7a2b",
"ownerId": null,
"attributes": { "protocol": "mqtt" },
"createdAt": "2026-08-24T14:25:01Z",
"updatedAt": null
}
}
}
kindon a Resource is a free-form string, not a fixed enum —"channel"is the value the UI and CLI use, butresourceKinds(tenantId)lists every kind that's actually in use for a workspace if you need to check what's already there.
Get Channel
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "query Channel(\$id: ID!) { resource(id: \$id) { id kind name alias tenantId ownerId attributes objectGroupIds createdAt updatedAt } }",
"variables": { "id": "9c2e7a41-3b5d-4f88-a2e0-6d1c8f4e9b73" }
}
EOFGet Channels
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "query ListChannels(\$tenantId: ID, \$kind: String, \$q: String, \$limit: Int, \$offset: Int) { resources(tenantId: \$tenantId, kind: \$kind, q: \$q, limit: \$limit, offset: \$offset) { total items { id kind name alias attributes createdAt updatedAt } } }",
"variables": { "tenantId": "d7f9b3b8-4f7e-4f44-8d47-1a6e5e6f7a2b", "kind": "channel", "limit": 20, "offset": 0 }
}
EOFAdditional filter arguments:
attributesContains,parentGroupId+includeDescendants,deleted,order(created_at/updated_at/name/kind),dir.
Update Channel
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation UpdateChannel(\$id: ID!, \$input: UpdateResourceInput!) { updateResource(id: \$id, input: \$input) { id name attributes updatedAt } }",
"variables": {
"id": "9c2e7a41-3b5d-4f88-a2e0-6d1c8f4e9b73",
"input": { "name": "Telemetry Channel (v2)", "attributes": { "protocol": "mqtt", "qos": 1 } }
}
}
EOFDelete Channel
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation DeleteChannel(\$id: ID!) { deleteResource(id: \$id) }",
"variables": { "id": "9c2e7a41-3b5d-4f88-a2e0-6d1c8f4e9b73" }
}
EOFPlatform-admin-only restoreResource(id: ID!): Boolean and purgeResource(id: ID!): Boolean mirror the Workspace/Device lifecycle above.
Enable/Disable and Connect/Disconnect have no direct equivalent
There is no enableResource/disableResource mutation (Resources don't carry the
active/inactive status Devices and Workspaces do), and no
connectResource/disconnectResource-style mutation linking a specific Device to a specific
Channel. A stored "connection" object doesn't exist in the current model.
Publish/subscribe access is now purely an authorization grant: create a Permission Block
scoped to the channel object (createPermissionBlock, action publish and/or subscribe),
then attach it to the device via createDirectPolicy (or via a Role, for a reusable grant
across many devices) — see Authorization for the full Permission
Block / Role / Direct Policy model.
Object Groups
addResourceToObjectGroup(resourceId, objectGroupId), removeResourceFromObjectGroup(resourceId, objectGroupId), and clearResourceObjectGroups(resourceId) manage a channel's membership in object groups — the same many-to-many mechanism Devices use (see Groups).
Groups
A Group is Atom's hierarchical structure for organizing entities — either an object group (grouping Devices/Channels) or a principal group (grouping Users, for use as a policy subject). The two are stored separately (group_type distinguishes them), but share the same query/lifecycle mutations.
Like the rest of this page, Groups are managed through Atom's GraphQL API: POST http://localhost:8080/graphql, Authorization: Bearer <token>, Content-Type: application/json, body {"query": "...", "variables": {...}}. Field/argument names below match Atom's schema (src/graphql/groups.rs, src/graphql/types/mod.rs) and the Groups CLI.
Create Group
There is one mutation per group type — both take the same CreateGroupInput and set groupType themselves:
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation CreateObjectGroup(\$input: CreateGroupInput!) { createObjectGroup(input: \$input) { id name tenantId groupType description parentId status attributes createdAt updatedAt } }",
"variables": {
"input": {
"name": "<group_name>",
"tenantId": "<workspace_id>",
"description": "<optional description>",
"attributes": {}
}
}
}
EOFUse createPrincipalGroup (identical input/output shape) to create a group of users instead. createGroupInput has no parentId field — to create a group under a parent, create it first and reparent it with setGroupParent (below).
Get Group / Get Groups
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "query Group(\$id: ID!) { group(id: \$id) { id name tenantId groupType description parentId status attributes createdAt updatedAt } }",
"variables": { "id": "2766ae94-9a08-4418-82ce-3b91cf2ccd3e" }
}
EOFcurl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "query ListGroups(\$tenantId: ID, \$q: String, \$limit: Int, \$offset: Int) { groups(tenantId: \$tenantId, q: \$q, limit: \$limit, offset: \$offset) { total items { id name groupType description parentId status createdAt } } }",
"variables": { "tenantId": "d7f9b3b8-4f7e-4f44-8d47-1a6e5e6f7a2b", "limit": 20, "offset": 0 }
}
EOFGet Group Children / Hierarchy
childGroups(parentId, limit, offset): GroupList— direct children of one group.objectGroups(q, tenantId, parentId, status, limit, offset): GroupList/principalGroups(q, tenantId, status, limit, offset): GroupList— list all groups of one type, optionally filtered by parent (there is no single combined "hierarchy" query; walkchildGroupsrecursively, or filter byparentIdon the type-specific list).
Update Group
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation UpdateGroup(\$id: ID!, \$input: UpdateGroupInput!) { updateGroup(id: \$id, input: \$input) { id name description attributes updatedAt } }",
"variables": {
"id": "2766ae94-9a08-4418-82ce-3b91cf2ccd3e",
"input": { "name": "Data Analysts", "description": "Analyzes sensor data" }
}
}
EOFSet / Remove Parent
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation SetGroupParent(\$id: ID!, \$parentId: ID!) { setGroupParent(id: \$id, parentId: \$parentId) { id parentId } }",
"variables": { "id": "2766ae94-9a08-4418-82ce-3b91cf2ccd3e", "parentId": "<parent_group_id>" }
}
EOFremoveGroupParent(id): Boolean clears it. Object groups have parallel setObjectGroupParent/removeObjectGroupParent mutations.
Enable / Disable / Suspend / Delete Group
enableGroup(id), disableGroup(id), and suspendGroup(id) each take just an id and return the updated Group (same three-state status Devices use). deleteGroup(id): Boolean soft-deletes.
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "mutation DisableGroup(\$id: ID!) { disableGroup(id: \$id) { id status } }",
"variables": { "id": "2766ae94-9a08-4418-82ce-3b91cf2ccd3e" }
}
EOFMembers (Assign / Unassign)
curl -sSiX POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user_token>" \
-d @- <<EOF
{
"query": "query GroupMembers(\$groupId: ID!) { groupMembers(groupId: \$groupId) { id kind name status } }",
"variables": { "groupId": "2766ae94-9a08-4418-82ce-3b91cf2ccd3e" }
}
EOFentityGroups(entityId): [ID] is the reverse lookup — every group one entity belongs to.
addEntityToObjectGroup(entityId, objectGroupId) and removeEntityFromObjectGroup(entityId, objectGroupId)
manage membership in object groups — membership is many-to-many, so adding twice is a
no-op, not an error. The old REST API's group-membership grant also carried explicit actions
(g_list, c_list, etc.) alongside the assignment; that's now a separate concern — see
Authorization's Roles/Permission Blocks, not part of group
membership itself.
Notifications
How Magistrala sends workspace-invitation emails today — via Atom's built-in mail sender, not a standalone Magistrala service.
Authorization
Magistrala's authorization model — Atom's Permission Blocks, Roles, and Direct Policies, and how Magistrala services (including Enterprise ones) check access against Atom.