Magistrala
Dev Guide

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": {}
    }
  }
}
EOF

Expected response:

{
  "data": {
    "signup": {
      "entityId": "b3f1c9a2-5e2d-4a11-9c3f-7a8b2e6d1f90",
      "email": "user@example.com",
      "verificationRequired": true
    }
  }
}

Notes:

  • Signup only succeeds if self_registration_enabled is set on the deployment; otherwise it returns "sign up is not enabled". Admin-created accounts (self-registration disabled) go through createEntity instead — 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.
  • email is normalized (lowercased) and used as the login identifier — it is not a field on the Entity type 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"
    }
  }
}
EOF

Expected response:

{
  "data": {
    "login": {
      "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
      "entityId": "b3f1c9a2-5e2d-4a11-9c3f-7a8b2e6d1f90",
      "sessionId": "9c7a1e2d-3b4f-4a5e-8d6c-1f2a3b4c5d6e"
    }
  }
}

Notes:

  • kind defaults to "password" if omitted; "shared_key" is also accepted (src/graphql/auth.rs's parse_login_credential_kind) for shared-key-based login.
  • To log into a specific workspace directly, pass tenantId or tenantAlias (the workspace's route) alongside identifier/secret — same LoginInput.
  • The returned token is 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": {}
    }
  }
}
EOF

Follow 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>" }
}
EOF

Update / 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>" }
}
EOF

Returns 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 the ATOM_URL environment 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" }
    }
  }
}
EOF

Expected 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:

  • name is required; alias (the route/slug used to address the tenant, successor to the old Domain alias) is optional but must be unique when set.
  • attributes is a free-form JSON object (successor to the old flat metadata field) — 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" }
}
EOF

Get 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" }
}
EOF

Supported 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" }
    }
  }
}
EOF

Enable / 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" }
}
EOF

Replace 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" }
}
EOF

Workspace 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 (requires q of 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 by inviteeUserId or inviteeEmail, optionally with a roleId; sends an email when inviteeEmail is 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" }
  }
}
EOF

Tip: 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" }
    }
  }
}
EOF

Expected 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 optionally profileVersionId); see Device Type Management.
  • There is no bulk/batch create mutation in Atom's GraphQL API. Each device must be created with its own createEntity call — 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" }
}
EOF

Get 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
  }
}
EOF

Additional 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 on attributes.is_gateway — there is no server-side isGateway filter 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

attributes in UpdateEntityInput replaces the whole attributes object, it does not merge — send the full desired object, not just the changed keys. alias and externalId use MaybeUndefined semantics: omit the field to leave it unchanged, or pass null explicitly 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" }
  }
}
EOF

The returned key is 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" }
}
EOF

Replace 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" }
}
EOF

Ownership

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>
  }
]
EOF

For 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-alive

Read 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 9009 for the Postgres Reader (HTTP)
  • Use 9011 for 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" }
    }
  }
}
EOF

Expected 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
    }
  }
}

kind on a Resource is a free-form string, not a fixed enum — "channel" is the value the UI and CLI use, but resourceKinds(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" }
}
EOF

Get 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 }
}
EOF

Additional 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 } }
  }
}
EOF

Delete 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" }
}
EOF

Platform-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": {}
    }
  }
}
EOF

Use 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" }
}
EOF
curl -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 }
}
EOF

Get 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; walk childGroups recursively, or filter by parentId on 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" }
  }
}
EOF

Set / 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>" }
}
EOF

removeGroupParent(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" }
}
EOF

Members (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" }
}
EOF

entityGroups(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.

On this page