REST API
Configure and control a Tangle project over HTTP and JSON. Tokens, scopes, conventions, errors and every route of /api/v1.
tangle-core serves a REST API at http://<host>:7340/api/v1. Every action the designer, dashboards and assistants can take is available here, with the same validation, scope checks and audit log. Request and response bodies use the Tangle contract types (the same JSON as the project file).
Base URL and documentation
| What | Where |
|---|---|
| API base | http://<host>:7340/api/v1 |
| OpenAPI 3.1 document | GET /api/v1/openapi.json (no token needed) |
| Interactive documentation | GET /api/docs (no token needed) |
The OpenAPI document is generated from the same route table that serves the API, so it always matches the running core. Its component schemas are the contract types. The /api/docs page loads its viewer from a public CDN, so the browser that opens it needs internet access.
By default tangle-core listens on 127.0.0.1:7340 (this machine only). To accept other machines, start it with --listen 0.0.0.0:7340 --allow-remote --insecure-http (see CLI). There is no TLS yet: use remote access on trusted networks only.
Authentication
Send an API token in the Authorization header:
GET /api/v1/project HTTP/1.1
Host: 127.0.0.1:7340
Authorization: Bearer tgl_0f3c...
Create tokens on the machine that runs tangle-core:
tangle-core token create --name stage-script --scopes read,control
tangle-core token list
tangle-core token revoke stage-script
- The secret (
tgl_followed by 64 hex characters) is printed once. Only its SHA-256 hash is stored, intokens.jsonin the Tangle data folder. - Without
--scopes, a token getsread,control. - A revoked token stops working at once, without restarting tangle-core.
- For clients that cannot set headers (a browser
EventSource), pass the token as?token=<token>in the query string. - A missing, unknown or revoked token gets
401with aWWW-Authenticate: Bearerheader.
For a core installed as a boot service, manage its tokens with sudo tangle-core token ... (macOS, Linux). See CLI.
Note: REST and MCP access for third-party software is part of Tangle Pro and Ultimate (and the demo mode). On other editions these requests get
403 not_licensed. The built-in designer and dashboards always work. See Licensing.
Scopes
| Scope | Allows |
|---|---|
read | Read the project, live state, timeline states, alerts, metrics, license, events. |
control | Send intents, drive timelines, run automations, acknowledge and silence alerts, push metrics, resume a paused session. |
configure | Change the project (create, replace, delete resources), test devices, scan the network, edit and publish dashboards, test notification channels. |
admin | Administration: licensing (activate, sign in, refresh, deactivate, offline), pairing and accepting cores. |
live | Lets an MCP assistant change things while a timeline plays. It has no effect on REST. See MCP. |
Scopes do not include each other: a token that should read and configure needs read,configure. A token without the scope a route needs gets 403 missing_scope.
Dashboard sessions
Browsers that open a dashboard get a short-lived session token from POST /api/v1/dashboards/{id}/session (no token needed to call it). Session tokens start with tgd_, last 12 hours and carry the read and control scopes. See Dashboards.
Conventions
JSON and resource ids
- Send
content-type: application/json. A body that is not JSON gets400 invalid_json. - Every resource has an
id: lowercase letters, digits,.,_and-, starting with a letter or digit, at most 128 characters. - Times are milliseconds since the Unix epoch (UTC), unless a field says otherwise.
- Cross-origin requests are allowed from any origin (requests carry bearer tokens, never cookies). The
ETagheader is exposed to browsers.
Errors
Errors use RFC 9457 problem details, with content type application/problem+json and a stable machine-readable code:
{
"type": "https://tangle.dev/problems/device_not_found",
"title": "Not found",
"status": 404,
"detail": "device proj-9 does not exist",
"code": "device_not_found"
}
Validation errors (422) add an errors array with one entry per problem. not_primary adds primary.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_json | The body is not JSON. |
| 400 | invalid_wait | ?wait= is not like 5s or 500ms. |
| 400 | topics_required | GET /events without ?topics=. |
| 400 | unsupported | The timeline does not support this transport operation. |
| 401 | unauthorized | Missing, unknown or revoked token, or expired dashboard session. |
| 401 | pin_required, wrong_pin | Dashboard session: PIN missing or wrong. |
| 403 | missing_scope | The token lacks the scope this route needs. |
| 403 | not_licensed | The edition does not include this (external API, monitoring, or the change exceeds a license limit). |
| 403 | panels_only | The dashboard accepts enrolled panels only. |
| 404 | <kind>_not_found | For example device_not_found, timeline_not_found, intent_not_found, automation_not_found. |
| 404 | no_draft | Publishing a dashboard that has no draft. |
| 409 | already_exists | POST with an id that is already used. |
| 409 | timeline_running | Editing a timeline (or its tracks and cues) while it plays, without allowLive. |
| 409 | show_mode | Network scan while a timeline plays. |
| 409 | show_running | MCP only: a change while a timeline plays, without the live scope. |
| 409 | not_primary | This node is a redundancy backup. primary holds the primary's address. |
| 409 | disabled | Running a disabled automation. |
| 409 | not_paused | Resuming when no designer or demo session is paused. |
| 409 | busy, conflict | The project changed too fast; try again. |
| 412 | precondition_failed | If-Match does not match the current ETag. |
| 422 | invalid | The body, or the project as it would be after the change, is invalid. |
| 429 | too_many_attempts | More than 5 wrong dashboard PINs in a minute. |
| 503 | unavailable | A service is not ready (stopped player, storage not configured, credential store error). |
ETags and If-Match
GET on a single resource (and on /project) returns a strong ETag. Send it back in If-Match on PUT or DELETE to make sure nobody changed the resource since you read it. If it changed, the request fails with 412 precondition_failed and nothing is applied.
If-Match: *matches any existing version.- A
PUTwith anIf-Match(other than*) on a resource that does not exist fails with412. - Successful writes return the new
ETag.
Dry runs
Add ?dryRun=true (or ?dryRun, ?dryRun=1) to any create, replace or delete. Core validates the change against the whole project and returns what would happen, without applying it:
{
"dryRun": true,
"result": { "id": "pj-2", "name": "Projector 2", "moduleId": "misc.pjlink.projector", "room": "hall" },
"changes": [{ "op": "create", "kind": "device", "id": "pj-2" }]
}
result is the resource as it would be (absent for a delete). changes lists contract ProjectChange entries (op: create, update or delete; kind; id; parent for tracks and cues, such as show or show/lights).
How changes are applied
- Each write is atomic. Core applies the change to a copy of the project, validates the whole result (schema and cross-references, such as a scene pointing at a device that no longer exists), then swaps it in. A mistake changes nothing and the
422lists each problem. - Changes are saved to the project file (JSON or YAML, following its extension) and recorded in
audit.login the log folder, with the token name and surface. - Devices are started or stopped to match.
- Device settings are checked against the module's
configSchema. Secret settings (markedx-secretin the module) are removed from the body and stored in the OS credential store, never in the project file. - Notification channel secrets (passwords, tokens, webhook URLs) also go to the credential store. The project keeps
********; send********back to keep the stored value. - A dashboard PIN sent as
access.pinis stored as a salted hash and never returned. - Every project change publishes a
project.changedevent.
Live timelines
Editing a timeline, or one of its tracks or cues, while it plays is refused with 409 timeline_running. Add ?allowLive=true to edit it anyway. Network discovery is always refused while any timeline plays (409 show_mode), because a scan floods the show network.
MCP assistants have an extra protection: without the live scope, every change and control action is refused while a timeline plays. REST clients are not subject to it. See MCP.
Redundancy: 409 not_primary
In a redundant cluster, only the primary accepts writes and control. A backup answers changes, intents, transport, automation runs, and alert acknowledgement or silencing with 409:
{
"type": "https://tangle.dev/problems/not_primary",
"title": "Conflict",
"status": 409,
"detail": "this node is a backup; send changes to the primary",
"code": "not_primary",
"primary": "http://10.0.0.1:7340"
}
primary is null while the backup does not know the primary yet. Reads work on every node. See Redundancy.
Routes
Scope is the scope the token needs. Paths are relative to http://<host>:7340.
Project and modules
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/project | read | The whole project (contract Project), with an ETag. Dashboard PIN hashes are left out. |
| GET | /api/v1/modules | read | Installed modules: [{ "manifest": ModuleManifest, "devices": ["pj-1", ...] }]. |
| GET | /api/v1/modules/catalog | read | Installed modules and the modules available to install (folders scanned on each call). |
| POST | /api/v1/modules/catalog/{id}/install | configure | Install or update a module from the catalog; devices using it restart. |
| POST | /api/v1/project/save | configure | Write the open project's file now (changes are also saved as they are made). Returns { savedAt, path }. |
| PUT | /api/v1/project/variables | configure | Declare the project's variables (a list of ProjectVariable). |
| PUT | /api/v1/project/units | configure | The project's units: metric or imperial, and overrides per quantity (UnitPreference). |
| PUT | /api/v1/project/controls | configure | The default dashboard control style and the highlight colours of each style. |
| PUT | /api/v1/project/cluster | configure | How the cluster shares the work: standby or shared mode, folder placement, auto return, node health limits (ClusterSettings). |
| GET | /api/v1/units | read | Every quantity and unit (°C, °F, W, kW, h…) with how to convert between them. |
| GET | /api/v1/templates | read | Project templates: the kinds of project the wizard can start from. |
| POST | /api/v1/templates/{id}/apply | configure | Add a template's rooms, planned devices, timelines, automations, dashboards and alert rules to the project (body: answers, devices; ?dryRun). |
Project library
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/projects | read | The library (ProjectLibrary): the open project (loaded: false when none) and every project on this core. |
| POST | /api/v1/projects | configure | Create an empty project (ProjectCreateRequest: name, optional id, open). |
| POST | /api/v1/projects/import | configure | Import a .tangle file or a project .json/.yaml as a new project: the file as the body, ?name=, ?open=true. |
| POST | /api/v1/projects/{id}/open | configure | Open a project; the open one closes. Refused with 409 show_running while a timeline plays, unless { "force": true }. Answers 202; clients get project.switching, then project.changed. |
| POST | /api/v1/projects/{id}/duplicate | configure | Copy a project with its dashboards and plans under a new name; open: true is Save as. |
| POST | /api/v1/projects/{id}/rename | configure | Give a project a new name (ProjectRenameRequest: name), open or not. Its id, dashboards and plans stay. Answers { "project": ProjectSummary }; renaming the open one sends project.changed. |
| GET | /api/v1/projects/{id}/export | read | One .tangle file (a zip: project, dashboards, plans, module lock; no device passwords). |
| DELETE | /api/v1/projects/{id} | configure | Delete a project, its dashboards, plans and alert history (not the open one). |
| POST | /api/v1/project/close | configure | Close the open project: devices stop and changes are refused until another opens. Same force rule as open. |
Project resources
Each resource type supports the same five routes:
| Method | Path | Scope | Description | Success |
|---|---|---|---|---|
| GET | collection path | read | List all resources of this type. | 200 with an array |
| POST | collection path | configure | Create one. The body must include id. | 201 with the resource, Location and ETag |
| GET | item path | read | Read one. | 200 with the resource and ETag |
| PUT | item path | configure | Create or replace one with a complete new version. The body id may be left out; if given it must match the path. | 200 (replaced) or 201 (created), with ETag |
| DELETE | item path | configure | Delete one. | 204 |
Create, replace and delete accept ?dryRun and ?allowLive; replace and delete accept If-Match.
| Resource | Collection path | Item path | Body (contract type) |
|---|---|---|---|
| Rooms | /api/v1/rooms | /api/v1/rooms/{id} | Room |
| Devices | /api/v1/devices | /api/v1/devices/{id} | DeviceInstance |
| Scenes | /api/v1/scenes | /api/v1/scenes/{id} | Scene |
| Timelines | /api/v1/timelines | /api/v1/timelines/{timeline} | Timeline |
| Tracks | /api/v1/timelines/{timeline}/tracks | /api/v1/timelines/{timeline}/tracks/{track} | Track |
| Cues | /api/v1/timelines/{timeline}/tracks/{track}/cues | /api/v1/timelines/{timeline}/tracks/{track}/cues/{id} | Cue |
| Dashboards | /api/v1/dashboards | /api/v1/dashboards/{id} | Dashboard |
| Automations | /api/v1/automations | /api/v1/automations/{id} | Automation |
| Alert rules | /api/v1/alertRules | /api/v1/alertRules/{id} | AlertRule |
| Notification channels | /api/v1/notificationChannels | /api/v1/notificationChannels/{id} | NotificationChannel |
| Maintenance windows | /api/v1/maintenanceWindows | /api/v1/maintenanceWindows/{id} | MaintenanceWindow |
| Probes | /api/v1/probes | /api/v1/probes/{id} | Probe |
| Listeners | /api/v1/listeners | /api/v1/listeners/{id} | Listener |
| Maps | /api/v1/maps | /api/v1/maps/{id} | Map |
| End-user views | /api/v1/views | /api/v1/views/{id} | EndUserView |
| Lighting sequences | /api/v1/sequences | /api/v1/sequences/{id} | Sequence |
| Lighting presets | /api/v1/presets | /api/v1/presets/{id} | LightingPreset |
| Lighting groups | /api/v1/lightingGroups | /api/v1/lightingGroups/{id} | LightingGroup |
| Executors | /api/v1/executors | /api/v1/executors/{id} | Executor |
The fields of each type are described in Project file. Notes per resource:
- Tracks and cues live inside their timeline: a missing parent timeline or track gets
404. Changing them counts as changing the timeline (timeline_runningapplies). - Dashboards: a
POSTorPUTpublishes the dashboard directly. Core increasesversion, keeps the published version in the dashboard's history, and screens showing it reload. To edit without affecting screens, use the draft routes. - Notification channels: Slack and Teams channels need
settings.webhookUrl.
Live state
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/snapshot | read | Every device's status and state, and every timeline's state (contract Snapshot: { devices: [DeviceSnapshot], timelines: [TimelineState] }). |
| GET | /api/v1/devices/{id}/state | read | One device: { device, status, state }. status is one of connecting, online, degraded, offline, timeout, error; state maps capability id to state values. |
| GET | /api/v1/state/timelines | read | Transport state of every timeline (array of TimelineState). |
| GET | /api/v1/state/listeners | read | Every listener: open or not, clients, message counts. |
| GET | /api/v1/cluster/placement | read | Shared mode: which node runs each device, and which devices moved off their preferred node. |
| POST | /api/v1/cluster/rebalance | control | Shared mode: spread the devices across the nodes again and bring moved devices back (dryRun to preview). |
| GET | /api/v1/cluster | read | Redundancy status (contract ClusterStatus): this node's role, the primary, quorum, clock offset and every node's health, sync state and cue log. A standalone node answers { "role": "standalone", "nodes": [] }. |
| GET | /api/v1/cluster/discovered | read | Cores found on the local network that are not in this cluster (contract DiscoveredCore), each with its state: new, separate or clustered. Only a primary or standalone core lists them. |
| POST | /api/v1/cluster/discovered/{id}/accept | admin | Accept a discovered core: body { "mode": "separate" } or { "mode": "redundant", "code": "482913" } (contract ClusterAccept). redundant needs that core's pairing code; a wrong code answers wrong_pairing_code. See Redundancy. |
| GET | /api/v1/cluster/pairing | admin | This core's current 6-digit pairing code, to type on the main core. |
| GET | /api/v1/events | read | Live events as Server-Sent Events. See Event stream. |
Intents
| Method | Path | Scope | Description |
|---|---|---|---|
| POST | /api/v1/intents | control | Send an intent (contract IntentRequest). |
| GET | /api/v1/intents/{intentId} | read | Current status of an intent (contract IntentStatus). |
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | power.on, power.off, level.set, mute.on, mute.off, input.select, transport.play, preset.recall (any <capability>.<action> of the standard capabilities power, level, mute, input, transport, preset); scene.recall with params.scene; action with params.capability, params.action, params.params; release to drop priority holds. |
target | object | yes | { "kind": "device", "id" }, { "kind": "room", "id" }, { "kind": "tag", "tag" } or { "kind": "all" }. |
params | object | no | Passed to the device action. holdMs keeps this intent's priority on the state it sets for that long. |
priority | integer 0-100 | no | Higher wins on conflict. Default 30 for API calls. 90 and above hold until a release. |
at | timestamp | no | When to run it. |
expiresAt | timestamp | no | Drop it if not run by then. |
Without ?wait, core answers 202 Accepted as soon as the intent is accepted, with a Location: /api/v1/intents/{intentId} header. With ?wait=5s (or 500ms, or a number of seconds; at most 60 s) it answers 200 with the final status, or the latest status when the time runs out.
Response (IntentStatus):
| Field | Description |
|---|---|
intentId | UUID of the intent. |
state | accepted, rejected, scheduled, running, completed, partial, failed or cancelled. |
reason | Why it was rejected or failed. |
devices | [{ "deviceId", "outcome": "ok" | "failed" | "overridden" | "skipped", "error" }]. |
updatedAt | Timestamp. |
Timelines
| Method | Path | Scope | Description |
|---|---|---|---|
| POST | /api/v1/timelines/{timeline}/transport | control | Play, pause, stop, seek or GO. Body { "op": "play" | "pause" | "stop" | "seek" | "go", "positionMs" }; positionMs is required for seek. Returns the new TimelineState. |
TimelineState fields: timelineId, status (stopped, playing, paused, waiting), positionMs, sampledAt (when positionMs was sampled, for interpolation), lastCueId, nextCueId, chasing, clockLost, anchor. See Timelines.
Devices: tests, discovery, logs and removal
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/devices/{id}/log | read | The device's log, newest first: ?limit, ?before, ?kinds (action, status, state, event, note), ?level (info, warning, error). |
| POST | /api/v1/devices/{id}/log | control | Write a note into the device's log: { "text", "level" }. |
| DELETE | /api/v1/devices/{id}/log | configure | Empty the device's log. |
| GET | /api/v1/devices/log | read | Logs of several devices merged, newest first: ?devices=a,b and the same filters. |
| POST | /api/v1/devices/references | read | What uses these devices and their sub-devices (body: ids): scenes, cues, automations, maps, dashboards, lighting, alert rules. |
| POST | /api/v1/devices/remove | configure | Delete devices, their sub-devices and their references in one change: body { "ids", "references": "remove" | "keep" } (keep turns their timeline cues into comment cues), ?dryRun. |
| POST | /api/v1/devices/{id}/configurators/{configurator}/run | configure | Run a setup assistant step through the device's module (body: step, answers, config). Settings it returns are saved (secrets to the credential store) and never sent back. |
| POST | /api/v1/devices/{id}/test | configure | Test a device's connection in a separate module process; the running device is not touched. Optional body { "config": { ... } } tests other settings before you save them (stored secrets are filled in). Returns { ok, status, detail, state, elapsedMs }; ok is false when the device did not come online. A test takes 10 s at most. |
| POST | /api/v1/discovery | configure | Scan the network (mDNS, SSDP and module searches). Optional body { "seconds": 1-30 }, default 5. Returns [{ address, name, manufacturer, model, via, service, suggestions }], where suggestions lists matching installed modules. Refused while a timeline plays (409 show_mode). |
Dashboards
Besides the resource routes, dashboards have a draft and a version history. Drafts and published versions are kept in the Tangle data folder.
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/dashboards/{id}/draft | configure | { "draft": true, "dashboard" } when a draft exists, otherwise { "draft": false, "dashboard" } with the published version. |
| PUT | /api/v1/dashboards/{id}/draft | configure | Save a draft (contract Dashboard). Screens keep the published version. Returns { "saved": true }. |
| POST | /api/v1/dashboards/{id}/publish | configure | Publish the draft, or the Dashboard in the body if one is sent. Screens reload to it. Returns { version, dashboard }. 404 no_draft without a draft or body. |
| GET | /api/v1/dashboards/{id}/versions | read | Published versions, newest first: [{ version, publishedAt, by, name }]. |
| GET | /api/v1/dashboards/{id}/versions/{version} | read | One published version: { version, publishedAt, by, dashboard }. |
| POST | /api/v1/dashboards/{id}/versions/{version}/restore | configure | Publish an old version again, as a new version. |
| POST | /api/v1/dashboards/{id}/session | none | Open a dashboard in a browser. Optional body { "pin": "1234" }. Returns { token, expiresAt, dashboardId, access }. |
Dashboard checks name the page and widget at fault, for example page main, widget w1: unknown widget type knob.
The session route follows the dashboard's access.mode: open dashboards get a session at once; pin dashboards need the right PIN (401 pin_required or wrong_pin, 429 too_many_attempts after 5 wrong PINs in a minute); panels dashboards answer 403 panels_only (enrolled panels use their own API token). A session stops working when it expires or when the dashboard switches to panels.
Automations
| Method | Path | Scope | Description |
|---|---|---|---|
| POST | /api/v1/automations/{id}/run | control | Run an automation now. Optional body { "force": true, "params": {} }: force skips its conditions, params fills its params. Returns the contract AutomationRun (runId, automationId, state, trigger, reason, step, startedAt, endedAt): started, or skipped with a reason. 409 not_primary on a backup. |
| POST | /api/v1/automations/{id}/stop | control | Stop every run of the automation in progress. |
| GET | /api/v1/automations/{id}/runs | read | The last 50 runs, newest first (kept in memory). |
| POST, GET | /api/v1/macros/{id}/run, /runs, /stop | control, read | Deprecated aliases (macros are automations since contract 0.2.30). run does not check conditions. |
See Automations.
Variables, controls and functions
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/variables | read | Project variables with their current values. |
| GET | /api/v1/variables/{name} | read | One project variable. |
| PUT | /api/v1/variables/{name} | control | Set a project variable: { "value": ... }. Variable triggers fire. |
| GET | /api/v1/controls | read | Dashboard controls with a kept value or override (value, label, visible, enabled, props). |
| GET | /api/v1/controls/{id} | read | One control's state. |
| PUT | /api/v1/controls/{id} | control | Set a control (ControlSetRequest: value, label, visible, enabled; null clears). Every screen follows and control triggers fire. |
| POST | /api/v1/controls/{id}/events | control | Report a control event (press, release, click or hold); control triggers waiting for it fire. |
| POST | /api/v1/controls/{id}/rename | configure | Rename a control: { "to" }. Every reference in the project and in dashboard drafts follows. |
| GET | /api/v1/functions | read | Every library function (core math, text, time, list, color, and library modules) with its signature. |
| POST | /api/v1/functions/{name}/call | control | Call a function: { "args": [...] } or named { "args": {...} }. Returns its value. |
Listeners
Besides the resource routes:
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/listeners/{id}/log | read | Messages the listener received, oldest first: ?since=<seq>&limit=<n>. |
| GET | /api/v1/listeners/{id}/runs | read | Recent runs of its rules' steps, newest first. |
| POST | /api/v1/listeners/{id}/test | configure | Check a message against its rules: { "text" } (or bytes, osc, http), dry run unless "run": true. |
See Listeners.
Device creator and community
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/drivers | read | Drivers made with the device creator. |
| GET | /api/v1/drivers/{id} | read | A driver's definition and code. |
| GET | /api/v1/drivers/{id}/files | read | Every file of the driver's module, to download it. |
| POST | /api/v1/drivers/check | configure | Check a definition and its code without saving. |
| POST | /api/v1/drivers | configure | Save a driver: builds and installs its module; devices using it restart. |
| DELETE | /api/v1/drivers/{id} | configure | Delete a driver: uninstalls its module and removes its source. 409 driver_in_use (with devices) while devices use it. |
| GET | /api/v1/community | read | The community server and who is signed in on this core. |
| POST | /api/v1/community/sign-in | configure | Sign in with the license account's email and password. |
| POST | /api/v1/community/register | configure | Choose the username (namespace) at the first sign-in. |
| POST | /api/v1/community/sign-out | configure | Sign out on this core. |
| POST | /api/v1/community/password-reset | configure | Email a password reset link. |
| GET | /api/v1/community/search | read | Search drivers: ?q=, scope=mine (default) or scope=all. |
| POST | /api/v1/community/install | configure | Install or update a community driver; devices using it restart. |
| POST | /api/v1/community/publish | configure | Publish a device creator driver under your namespace. |
See Device creator.
Maps, plans and images
| Method | Path | Scope | Description |
|---|---|---|---|
| POST | /api/v1/plans | configure | Import a map plan: the file as the body (SVG, DXF, DWG, PNG or JPEG), ?name=. DWG needs a converter on the core's computer. |
| GET | /api/v1/plans/{file} | read | A stored plan file. |
| GET | /api/v1/maps/{id}/positions | read | Placed devices and live positions from tracking sources. |
| POST | /api/v1/positions | control | Push positions from a tracking source: 1 to 1000 positions, in map metres. Returns { accepted, rejected }. |
| POST | /api/v1/assets | configure | Upload a dashboard image (PNG, JPEG, WebP, GIF or SVG, 10 MB at most): the file as the body, ?name=. |
| GET | /api/v1/assets/{file} | read | A stored dashboard image. Its name is its content hash, so it never changes. |
| GET | /api/v1/views/public | none | The end-user views a browser can open (id, name, skin, theme, access mode). |
| POST | /api/v1/views/{id}/session | none | Open an end-user view in a browser; returns a session token. |
Lighting
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/lighting/state | read | Every sequence's live cue, level and pause, the grand master, and your programmer. |
| POST | /api/v1/lighting/command | control | Play lighting: go, back, goto, off, fader, flash, pause a sequence, or the grand master. |
| POST | /api/v1/lighting/programmer | control | Change your programmer: select lights, set values, fan, blind, highlight, clear. |
| POST | /api/v1/lighting/store | configure | Store the programmer into a cue, a preset or a group. |
| GET | /api/v1/lighting/library | read | Search the bundled fixture library: ?q=<manufacturer or model>&limit=50. |
Sentinel (monitoring)
Monitoring is part of Tangle Pro and Ultimate. See Sentinel.
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/sentinel | read | Overview: active, licensed, counts (critical, warning, info, pending), firing alerts, rooms, health of devices, cores and probes, metrics count, notificationsQueued. |
| GET | /api/v1/sentinel/alerts | read | { "alerts": [Alert] } (pending and firing). ?history=true adds history (recent transitions, newest first); ?limit= caps it (default 100). |
| POST | /api/v1/sentinel/ack | control | Acknowledge an alert: { "alertId", "note" }. Stops repeat notifications. Returns the alert. |
| POST | /api/v1/sentinel/silence | control | Silence one alert or every alert of a rule: { "alertId" } or { "ruleId" }, plus until (timestamp in the future, required). Returns { until, alerts, ruleId }. |
| POST | /api/v1/sentinel/metrics | control | Push custom metric samples: [{ "name", "value", "labels", "at" }] (1 to 1000 per call; { "samples": [...] } also works). Returns { "accepted": n }. |
| GET | /api/v1/sentinel/metrics | read | Current custom and probe metric values. ?samples=true adds 24 h of per-minute samples. |
| POST | /api/v1/sentinel/channels/{id}/test | configure | Send a test notification. Returns { ok, channelId, error }. |
Alert ids look like <ruleId>/<subject kind>:<subject id>, for example offline/device:proj-1. Acknowledging and silencing run on the primary only (409 not_primary on a backup).
License
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/license | read | Contract LicenseInfo: mode (licensed, designer, demo), status (state, edition, entitlements, limits, dates), features, session (paused, endsAt) in designer or demo mode, pendingRestart, notes. |
| POST | /api/v1/license/resume | control | Resume device connections and timelines after the hourly pause of the free designer or demo mode. Returns the new LicenseInfo. 409 not_paused when nothing is paused. |
| POST | /api/v1/license/activate | admin | Activate this computer with a license key (applies at the next start). |
| POST | /api/v1/license/sign-in | admin | Sign in with the license owner's email and password; lists their licenses, or activates the one chosen. |
| POST | /api/v1/license/password-reset | admin | Email a password reset link to a license owner. |
| POST | /api/v1/license/request-file | admin | A license request for this computer, to email to Tangle licensing. |
| POST | /api/v1/license/import | admin | Install a license file made elsewhere (offline activation, or the answer to a request). |
| POST | /api/v1/license/refresh | admin | Fetch a fresh license file now. |
| POST | /api/v1/license/deactivate | admin | Remove this computer from its installation. |
| PUT | /api/v1/license/offline | admin | Offline mode: never contact the license server. |
See Licensing.
Event stream (SSE)
GET /api/v1/events?topics=<list> streams live events as Server-Sent Events. topics is required: a comma-separated list of <event type>:<subject id> or <event type>:*.
curl -N "http://127.0.0.1:7340/api/v1/events?topics=device.state:*,timeline.state:show" \
-H "Authorization: Bearer $TOKEN"
event: device.state:proj-1
data: {"type":"device.state","deviceId":"proj-1","capability":"tangle.power","values":{"on":true},"at":1760000000000}
- Each SSE event name is the topic; the data is the event JSON (contract
TangleEvent). - A client that falls behind gets an event named
laggedwith{"missed": n}. - A keep-alive comment is sent every 15 seconds.
- The topics are the same as on the WebSocket. See WebSocket events for the list and each event's fields.
In a browser, use EventSource with ?token=:
const es = new EventSource(`/api/v1/events?topics=device.status:*&token=${token}`);
es.addEventListener("device.status:proj-1", (e) => console.log(JSON.parse(e.data)));
Examples
Set a shell variable with your token first:
export TOKEN=tgl_...
Read the project:
curl http://127.0.0.1:7340/api/v1/project -H "Authorization: Bearer $TOKEN"
Recall a scene in a room and wait up to 5 s for the result:
curl -X POST "http://127.0.0.1:7340/api/v1/intents?wait=5s" \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{"name":"scene.recall","target":{"kind":"room","id":"room-a"},"params":{"scene":"presentation"}}'
Check a new device before adding it, then add it:
curl -X POST "http://127.0.0.1:7340/api/v1/devices?dryRun=true" \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{"id":"pj-2","name":"Projector 2","moduleId":"misc.pjlink.projector","room":"room-a","config":{"host":"10.0.0.21"}}'
curl -X POST http://127.0.0.1:7340/api/v1/devices \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{"id":"pj-2","name":"Projector 2","moduleId":"misc.pjlink.projector","room":"room-a","config":{"host":"10.0.0.21"}}'
Update a scene safely (read its ETag, then send it back):
curl -i http://127.0.0.1:7340/api/v1/scenes/presentation -H "Authorization: Bearer $TOKEN"
# ETag: "3f2a9c1d0b7e4a55"
curl -X PUT http://127.0.0.1:7340/api/v1/scenes/presentation \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
-H 'If-Match: "3f2a9c1d0b7e4a55"' \
-d '{"id":"presentation","name":"Presentation","actions":[{"deviceId":"pj-2","capability":"tangle.power","action":"on"}]}'
Start a timeline and jump in a cue list:
curl -X POST http://127.0.0.1:7340/api/v1/timelines/show/transport \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" -d '{"op":"play"}'
curl -X POST http://127.0.0.1:7340/api/v1/timelines/show/transport \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" -d '{"op":"seek","positionMs":90000}'
Push a metric from a script:
curl -X POST http://127.0.0.1:7340/api/v1/sentinel/metrics \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '[{"name":"room.temperature","value":31.5,"labels":{"room":"server"}}]'