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.
Concepts
Section titled “Concepts”- A workspace is the data boundary. Every endpoint is under
/api/v1/workspaces/{ws}/..., where{ws}is a route (for examplecold-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 answer403 pat.not_allowed.
Creating a token
Section titled “Creating a token”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):
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.
Scopes
Section titled “Scopes”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.
Authentication
Section titled “Authentication”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.
export CELESTIAN=https://celestian.example.comexport TOKEN=tpat_…export WS=cold-storagecurl -s "$CELESTIAN/api/v1/workspaces/$WS" -H "Authorization: Bearer $TOKEN"Responses and errors
Section titled “Responses and errors”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.
Lists, filters, and pagination
Section titled “Lists, filters, and pagination”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).