Skip to content

API introduction

There are two ways to talk to Celestian programmatically:

For Protocol Section
Applications, scripts, and integrations that manage or read data HTTP + JSON with a Personal Access Token (PAT) This page and the complete worked example
Devices that send data MQTT with a device ID and key Connecting devices

The general data flow: a device publishes to a channel over MQTT → a rule (Channel Subscriber → Internal DB) stores it → it is read through the API, the Messages page, or a dashboard. Without a storage rule, messages are received but not stored.

  • A workspace is the data boundary. Every endpoint is under /api/v1/workspaces/{ws}/..., where {ws} is a route (for example cold-storage) or a UUID.
  • A PAT belongs to one user and is locked to one workspace. Effective rights = that user’s rights ∩ the token’s scope. A token cannot grant more than its owner holds.
  • With a PAT only the /api/v1/workspaces/... routes can be used. Managing tokens, the profile, and access-management actions (roles, members, policies) are unavailable. Other routes answer 403 pat.not_allowed.

The easiest way: in the application, Profile → Personal Access Tokens → Create. Choose the workspace, a lifetime (1 to 365 days), and the rights (scopes). The token value is shown only once. If it is lost, create a new one or use Regenerate.

Through the API (needs a signed-in session, not a PAT):

Terminal window
curl -X POST https://celestian.example.com/api/v1/users/me/tokens \
-H "Authorization: Bearer <session-access-token>" -H "Content-Type: application/json" \
-d '{
"name": "sample-integration",
"description": "read and write warehouse data",
"workspaceId": "<workspace-uuid>",
"durationDays": 30,
"blocks": [
{"scope": "workspace", "actions": ["read"], "effect": "allow"},
{"scope": "channel", "actions": ["read","write","connect","publish","subscribe"], "effect": "allow"},
{"scope": "device", "actions": ["read","write","connect"], "effect": "allow"},
{"scope": "rule", "actions": ["read","write","execute"], "effect": "allow"}
]
}'

The 201 response:

{
"secret": "tpat_nuZbn7p9P…",
"token": {
"id": "…", "name": "sample-integration", "workspaceRoute": "cold-storage",
"prefix": "tpat_nuZb", "durationDays": 30,
"expiresAt": "2026-11-01T22:41:37+07:00", "lastUsedAt": null, "status": "active",
"blocks": [ … ]
}
}

secret (prefixed tpat_) is what goes in the header.

effect: "allow" only. Access-management actions are refused for a PAT: manage, member.invite, role.manage, policy.manage, and *. Each block has a scope, actions, and an optional objectId or tags restriction. The complete action catalog is in the Permission catalog and at GET /api/v1/permissions/catalog.

The principle: give a token only the actions it needs. A reader integration needs just channel.read.

Authorization: Bearer tpat_<token>

The base address follows the application’s public address:

https://<domain>/api/v1/workspaces/{ws}/...

Only Bearer tpat_… is accepted, and the request body is limited to 2 MiB. A revoked or expired token answers 401.

Terminal window
export CELESTIAN=https://celestian.example.com
export TOKEN=tpat_…
export WS=cold-storage
curl -s "$CELESTIAN/api/v1/workspaces/$WS" -H "Authorization: Bearer $TOKEN"

Success: JSON according to the endpoint, or 204 without a body. An error always has one shape:

{ "code": "validation_failed", "message": "input tidak valid", "fieldErrors": { "route": "channel.route_invalid" } }

code is stable and safe for programs to handle; message is only for humans; fieldErrors (optional) maps field names to error codes. The code list: Error codes.

A list endpoint returns the objects under a plural key together with paging:

{ "channels": [ { "id": "…", "name": "Warehouse Telemetry", "route": "warehouse-telemetry", "tags": ["telemetry"], "status": "enabled" } ],
"total": 1, "page": 1, "limit": 10 }

Common query: ?search=&field=name|route|tag|id&status=enabled|disabled|all&sort=&page=1&limit=20 (limit: 10, 20, 50, 100). For field=tag: a,b = either (OR), a+b = all (AND).

Messages use a time cursor, not page numbers: pass the cursor from page.nextCursor again for the next page. Times use RFC 3339 (2026-10-02T00:00:00Z).