Projects & Builds
A project is an AppBlocks application: a target device, a set of features (peripherals, protocols, logic) and the generated source code. A project version is an immutable snapshot of a project together with its compiled firmware — this is what gets deployed to devices over the air.
Base paths: /api/projects, /api/tasks, /api/workspace-projects.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/projects/examples | public | List example projects |
| GET | /api/projects/examples/categories | public | Example categories |
| GET | /api/projects/examples/:id | public | One example with README |
| GET | /api/projects/:id? | session | List projects, or get one |
| POST | /api/projects | session | Create a project |
| PUT | /api/projects/:id? | session | Update a project |
| DELETE | /api/projects/:id | session | Delete a project |
| DELETE | /api/projects | session | Delete several projects |
| POST | /api/projects/code | public | Generate source code |
| POST | /api/projects/download | public | Download a project as a ZIP |
| POST | /api/projects/save | public | Alias of /download |
| POST | /api/projects/blockgen | public | Generate blocks with AI |
| POST | /api/projects/build | public | Build synchronously |
| GET | /api/projects/:id/versions/:versionCode? | session | List or get project versions |
| POST | /api/projects/:id/versions | session | Create a version |
| PUT | /api/projects/:id/versions/:versionCode | session | Upload binaries for a version |
| DELETE | /api/projects/:id/versions/:versionCode | session | Delete a version |
| GET | /api/projects/:id/versions/:versionCode/:filename | public | Download the application binary |
| GET | /api/projects/:id/versions/:versionCode/firmware/:filename | public | OTA firmware download (.tcu) |
| GET | /api/projects/:id/versions/:versionCode/release/:filename | public | Release binary download |
| POST | /api/tasks | optional | Queue a build job |
| GET | /api/tasks/:id | optional | Poll a build job |
| DELETE | /api/tasks/:id | optional | Cancel a build job |
| POST | /api/workspace-projects/export | session | Export a workspace as a bundle |
| POST | /api/workspace-projects/import | session | Import a bundle |
| POST | /api/workspace-projects/update | session | Update a workspace from a newer bundle |
session in the Auth column means the request must be authenticated — an
API key works just as well as a signed-in session. See
Authentication.
Access rules
- A project with no
workspacebelongs to its creator. - A workspace project is readable by anyone with workspace access.
- A workspace project is writable by the workspace owner, admins, operators
and members — but not by tenant-only users, who get
403 {"error":"Tenant users are not allowed to edit projects"}. - Ownership fields are set at creation time. A
PUTcannot move a project between users or workspaces.
Projects
List projects
GET /api/projects
| Query parameter | Description |
|---|---|
start | Offset, default 0 |
count | Page size, default 10 |
search | Case-insensitive match on project name |
workspace | Return that workspace's projects instead of personal ones |
<field>___<op> | Field filters |
{
"count": 12,
"data": [
{
"_id": "664a1f2b8c9d0e0011223301",
"name": "Boiler control",
"shortDescription": "Two-zone boiler controller",
"description": "…",
"device": { "platform": "…", "name": "…" },
"features": [ /* … */ ],
"runtime": "tios",
"version": "3",
"workspace": "664a1f2b8c9d0e0011223344",
"updatedAt": "…"
}
]
}
Internal node_* features are stripped from list results. Requesting a
workspace you do not belong to returns 404 {"error":"Workspace Not Found"}.
Get one project
GET /api/projects/:id
Returns the same envelope with a single-element data array (and count: 0).
Unknown ids return 404 {"error":"Project Not Found"}.
Create and update
POST /api/projects # create
PUT /api/projects/:id # update
{
"name": "Boiler control",
"description": "Full description",
"shortDescription": "Two-zone boiler controller",
"device": { "platform": "…", "name": "…" },
"features": [ /* feature definitions */ ],
"featureFlags": { },
"featureFlagMap": { },
"version": "3",
"runtime": "tios",
"workspace": "664a1f2b8c9d0e0011223344"
}
workspace is honoured only when creating; the caller must belong to it.
runtime is the target runtime (for example tios or zephyr). The full saved
project is returned.
Delete projects
DELETE /api/projects/:id # one
DELETE /api/projects # many
{ "projects": ["664a1f2b…", "664a1f2c…"] }
Bulk deletion is all-or-nothing: if the caller cannot edit every listed
project, nothing is deleted and the response is
403 {"error":"Unauthorized"}.
Code generation and download
POST /api/projects/code
{
"params": {
"project": { /* project configuration */ },
"debug": true
}
}
Returns the generated files as an array of { name, contents }. debug
defaults to true.
POST /api/projects/download
POST /api/projects/save
The body is the project configuration itself (not wrapped in params). The
response is a ZIP (application/octet-stream,
Content-Disposition: attachment; filename=<project name>.zip) containing the
generated sources. For the tios runtime the required platform files are bundled
in as well. Both paths run the same handler.
POST /api/projects/blockgen
{ "prompt": "blink an LED every second", "project": { /* … */ } }
Uses the AI block generator to produce blocks for the given project. Returns the generated blocks as JSON.
Builds
Building is normally asynchronous: submit a job, then poll it. Jobs are dispatched to build workers connected over Socket.IO and are deleted 3 minutes after creation, so poll promptly.
Queue a build
POST /api/tasks
{
"command": "build",
"files": [ { "name": "main.tbs", "contents": "…" } ],
"project": { /* project configuration, incl. runtime and device */ },
"debug": true,
"requireSymbols": false
}
{ "status": "running", "output": { "puuid": "66c1a0f2…" } }
puuid is the job id. requireSymbols: true keeps the symbol/ELF file in the
result (needed for debugging and for OpenOCD upload).
command must be "build". Any other value produces no response at all, so the
request hangs until the client times out.
Poll a build
GET /api/tasks/:id
status | Meaning | data |
|---|---|---|
running | Still queued or compiling | { output, progress } |
completed | Success | { output, binary, symbols, hex } |
aborted | Compilation failed, or required symbols were missing | { output } |
unknown | Job not found — invalid, cancelled or already expired | { output } |
Binaries are returned as buffer payloads. For tios builds the compiler output is
a .tpc/.pdb pair, which the response exposes as binary and symbols.
Polling somebody else's job returns 403.
Server-side errors are reported as 200 with
{"status":"error","data":{"output":"server error"}}.
Cancel a build
DELETE /api/tasks/:id
Deletes the job. Returns {"status":"aborted","data":{"output":"Compilation was aborted."}}.
Cancelling another user's job returns 403.
Synchronous build
POST /api/projects/build
Same body as POST /api/tasks (without command). The request blocks until the
build finishes and returns the artifacts directly:
{
"tpc": "<buffer>",
"pdb": "<buffer>",
"binary": "<buffer>",
"hex": "<buffer>",
"symbols": "<buffer>",
"output": "compiler output",
"project": { /* echoed back */ },
"status": "completed"
}
Failures return 500 { "output": "<compiler output>" }. This endpoint is meant
for tooling such as the desktop IDE; prefer /api/tasks for anything
interactive, since it does not hold a connection open for the whole build.
Project versions
A version pins a project's configuration and carries the compiled artifacts:
binary— the application/firmware image served as a.tcuOTA downloadreleaseBinary— an optional release image served as a.bindownloadurl/releaseUrl— absolute download URLs generated by the server
List or get versions
GET /api/projects/:id/versions
GET /api/projects/:id/versions/:versionCode
The list form returns metadata only (no binaries):
{
"count": 4,
"data": [
{
"_id": "664a1f9c8c9d0e0011223377",
"name": "Boiler control",
"versionCode": "13",
"versionDescription": "Add second zone",
"version": "3",
"device": { /* … */ },
"createdAt": "…",
"url": "https://api.appblocks.io/api/projects/…/firmware/….tcu",
"releaseUrl": "https://api.appblocks.io/api/projects/…/release/….bin"
}
]
}
For the single-version form, :versionCode is the version document's _id — not
its versionCode field. Download URLs, by contrast, use the versionCode.
Create a version
POST /api/projects/:id/versions
{
"versionCode": "13",
"versionDescription": "Add second zone",
"name": "Boiler control",
"description": "…",
"shortDescription": "…",
"device": { /* … */ },
"features": [ /* … */ ],
"version": "3",
"runtime": "tios"
}
versionCode must be unique within the project. Returns the created version.
Requires edit rights on the project.
Upload binaries
PUT /api/projects/:id/versions/:versionCode
Content-Type: multipart/form-data
| Field | Type | Notes |
|---|---|---|
binary | file | Required. The firmware image |
releaseBinary | file | Optional release image |
firmwareVersion | text | Used in the generated filename and stored on the version |
runtime | text | Target runtime |
workspace, project, name | text | Accepted by the upload handler |
Here :versionCode is the version's versionCode field. The server generates the
download URLs and returns 204 No Content:
<API_URL>/api/projects/<projectId>/versions/<versionCode>/firmware/<projectId>_<versionCode>_<firmwareVersion>.tcu
<API_URL>/api/projects/<projectId>/versions/<versionCode>/release/<projectId>_<versionCode>_<firmwareVersion>.bin
Feed the first URL to device firmware assignment or to group firmware assignment.
Delete a version
DELETE /api/projects/:id/versions/:versionCode
:versionCode is the version's _id. Requires edit rights on the project.
Downloads
GET /api/projects/:id/versions/:versionCode/firmware/:filename # OTA image (.tcu)
GET /api/projects/:id/versions/:versionCode/:filename # application binary
GET /api/projects/:id/versions/:versionCode/release/:filename # release image (.bin)
All three are public so devices can fetch updates without credentials, and all
respond with application/octet-stream. The :filename only sets the download
name; the content is determined by the project and version.
The OTA endpoint streams in 32 KB chunks and logs progress server-side, using the
requesting IP (or the matching device's MAC) to label the transfer — this is what
produces the OTA update <mac> - <percent>% lines in the server log. The plain
binary endpoint trims everything before the TBIN marker so the response is the
bare application image.
Example projects
GET /api/projects/examples
GET /api/projects/examples/categories
GET /api/projects/examples/:id
Examples are read from the server's public/examples directory. /examples
returns { data, count } where each entry has id, name, device,
description, image and any extra metadata from the example's project.json.
/examples/categories returns an array of categories, each with a projects
array of { id, name }. /examples/:id adds the rendered README.md as
content and the full project configuration as project, ready to be posted to
/api/projects.
Workspace bundles
Bundles move a whole workspace — projects, versions with firmware, device groups, workflows, dashboards and SCADA screens — between servers or workspaces.
Export
POST /api/workspace-projects/export
{
"workspaceId": "664a1f2b8c9d0e0011223344",
"bundleVersion": "2026.08.1",
"includeBinaries": true
}
Requires workspace membership. The response is a JSON bundle sent as a file attachment:
{
"_exportVersion": "1.1.0",
"_bundleId": "664a1f2b8c9d0e0011223344",
"_bundleVersion": "2026.08.1",
"_exportedAt": "2026-08-17T06:00:00.000Z",
"_sourceServerUrl": "https://api.appblocks.io",
"_sourceWorkspace": { "id": "664a1f2b…", "name": "Acme Facilities" },
"workspace": { "name": "Acme Facilities", "variables": { "MQTT_HOST": "__PLACEHOLDER__" }, "tenantVariableTemplates": [] },
"projects": [],
"projectVersions": [],
"deviceGroups": [],
"workflows": [],
"pageApps": [],
"scadaScreens": []
}
Workspace variable values are replaced with __PLACEHOLDER__ so secrets do
not travel with the bundle; set them again after import. Firmware binaries are
base64-encoded into the bundle unless includeBinaries: false, in which case
snapshots carry metadata only and their download URLs will not resolve on the
target server. Devices, tenants and members are never exported.
Import
POST /api/workspace-projects/import
{
"bundle": { /* exported JSON */ },
"workspaceName": "Acme Facilities (copy)",
"workspaceId": "664b9a1c8c9d0e00112255aa"
}
With workspaceId, the bundle's contents are added to that existing workspace
(requires edit rights). Without it, a new workspace is created and owned by the
caller. Fresh ids are generated for every entity while cross-references are
preserved, and firmware download URLs are rewritten to the importing server.
{
"message": "Workspace project imported successfully",
"workspace": { /* … */ },
"importedIntoExisting": false,
"counts": { "projects": 3, "projectVersions": 7, "deviceGroups": 2, "workflows": 4, "pageApps": 1, "scadaScreens": 0, "tenantVariableTemplates": 2 }
}
Update from a newer bundle
POST /api/workspace-projects/update
{
"bundle": { /* newer export of the same bundle */ },
"workspaceId": "664b9a1c8c9d0e00112255aa",
"removeOrphans": false
}
Entities are matched by their _bundleSourceId, so this behaves like an upgrade
rather than a duplicate import: matched entities are replaced in place (keeping
their local _id, workspace and owner), new entities are inserted, and entities
whose source is no longer in the bundle are deleted when removeOrphans is
true — otherwise left untouched. Project versions also match on their natural
sourceProject + versionCode key, so snapshots created before bundle tracking
still line up.
A bundle without _bundleId cannot be matched and returns 400.
{
"message": "Workspace updated from bundle successfully",
"workspace": { /* … */ },
"orphansRemoved": false,
"counts": {
"projectsUpdated": 3, "projectsInserted": 1, "projectsOrphaned": 0,
"deviceGroupsUpdated": 2, "deviceGroupsInserted": 0, "deviceGroupsOrphaned": 1
}
}