Reference
API reference.
A versioned HTTP API for interpreting and analyzing PLC projects.
The PLCs.ai API provides plain-language interpretation, troubleshooting, and analysis of PLC projects behind a versioned, externally-authenticated HTTP surface.
Scope. The API reasons over projects your organization already owns. It does not onboard a machine: a project enters the platform through the app or a version-source connector, and there is no create-a-project verb here. Once a project exists you can read it, ask questions about it, propose changes, and save new versions of it.
Platform support. Three readers cover the platforms we read: Rockwell Allen-Bradley (.L5X from Studio 5000), Siemens TIA Portal (an Openness .zip export) and CODESYS V3 (.export).
The CODESYS reader covers the OEM toolchains built on CODESYS V3. All of them carry the same vendor value — codesys — and the same .export format, so there is one API surface for the family rather than one per brand:
| Brand | Supported product line | Export comes from | |---|---|---| | CODESYS V3 | Any V3 controller | CODESYS V3 | | WAGO | PFC100 / PFC200 | e!COCKPIT | | ABB | AC500 V3 | Automation Builder | | Eaton | XC / XV | XSoft-CODESYS-3 | | Bosch Rexroth | ctrlX CORE | ctrlX WORKS | | Festo | CPX-E-CEC | CODESYS V3 | | Lenze | c300 / p300 / c430 / c520 / c550 | PLC Designer | | ifm electronic | ecomatController / ecomatmobile | CODESYS V3 | | Phoenix Contact | PLCnext | CODESYS Control for PLCnext | | Turck | TX500 HMI/PLC, TBEN-L-PLC | CODESYS V3 |
Coverage is part of the contract, so this table is published rather than implied. Where a cell says 422, that endpoint returns 422 vendor_unsupported with a body naming the supported alternative — it never returns an answer produced for a different platform. Every verb currently covers all three platforms; the notation stays because a platform joins verb by verb.
| Endpoint | Allen-Bradley | Siemens | CODESYS | |---|---|---|---| | GET /projects/{id} | yes | yes | yes | | GET /projects/{id}/source | yes | yes | yes | | POST /projects/{id}/versions | yes | yes | yes | | POST /projects/{id}/interpret | yes | yes | yes | | POST /projects/{id}/generate | yes | yes | yes | | POST /projects/{id}/conversations | yes | yes | yes | | GET /projects/{id}/analysis | yes | yes | yes | | POST /projects/{id}/analyses | yes | yes | yes | | GET /analyses/{analysisId} | yes | yes | yes | | POST /projects/{id}/exports/plc | yes | yes | yes | | POST /projects/{id}/exports/pdf | yes | yes | yes |
What a PDF report contains differs by platform, because the report is assembled from what the stored parse can supply on the server. Allen-Bradley and CODESYS reports carry the full structural inventory and their drawn logic — for Allen-Bradley the project tree, controller, tags, UDTs, AOIs, RLL rungs and FBD/SFC sheets; for CODESYS the project overview and import receipt, the device and task configuration, the program units with their ST bodies, global variables and located I/O, DUTs and interfaces, the HMI screen inventory and signal map, the LD networks and the CFC sheets. A Siemens report carries the aggregated SCL and the pipeline analysis chapters only: its block, tag and UDT chapters and its LAD/FBD/GRAPH diagrams are drawn by the in-app export, not here. PackML is an Allen-Bradley-only chapter on every lane.
generate on a CODESYS project runs the same two-step flow as the other two platforms — propose a plan, approve it, then get code — and the proposed changes are CODESYS-shaped. A change's code_type is ST (the complete Structured Text body for the unit), Declaration (the unit's complete declaration, for a new variable, DUT, interface, POU, method or property), LD (a rung-edit script rather than source, because a CODESYS ladder body is edited rung by rung) or Task (a task-configuration change, whose content is a program name or the new task's fields). As on every platform, nothing is persisted: commit a proposal with POST /projects/{id}/versions (the separate code_write scope), or make the change in the in-app assistant instead.
Versioning. The /api/v1 path is a namespace, not a contract version — it does not change when the contract does. What identifies the contract is info.version above, which is the date this contract last changed rather than a semantic version. That is deliberate: there is currently no way to request an earlier contract — no version header, no date parameter — so a semver number would imply a pinning guarantee that does not exist.
This contract is not yet frozen. Until it is, breaking changes land in place and move the date; there is no parallel older surface to fall back to. Check the date before relying on a response shape, and pin your client library rather than the URL. A version header for requesting a specific contract date is planned; when it ships, this section will say so.
Authentication. Every request carries an API key as a bearer token: Authorization: Bearer plck_live_…. A key resolves its organization from the credential itself — there is no organization in the URL. Mint, scope, and revoke keys from Settings → API Keys in the app.
Idempotency. Every write accepts (and requires) an Idempotency-Key header so a retry never creates a duplicate billable unit.
Errors. Every error uses one envelope with a human userMessage, a suggestedAction, and an isRetryable flag. Every response — success or error — carries a unique request-id header; quote it in support requests.
Permissions. A key carries a set of scopes. Each endpoint documents the scope it requires.
Download the OpenAPI 3.1 spec (YAML) — the same file the SDKs use.
Base URL
| Server | Description |
|---|---|
https://app.plcs.ai/api/v1 | Production |
Authentication
An API key minted from Settings → API Keys, sent as Authorization: Bearer plck_live_….
Authenticated health check
Confirms the API is reachable and your credential resolves an organization. Returns the resolved organizationId and actorType.
Responses
| Status | Description |
|---|---|
| 200 | The credential resolved an organization. |
| 401 | No valid credential, or the key was revoked. |
Response · 200
| Field | Type | Description |
|---|---|---|
status required | const "ok" | |
organizationId required | string | — |
actorType required | string enum |
{
"status": "ok",
"organizationId": "string",
"actorType": "api_key"
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Ask a question about a project
The headline capability: a question in, a grounded answer out.
The assistant is interactive — when your question is ambiguous it can stop and ask rather than guessing. So a successful response is one of two shapes, discriminated by status:
status: "answer"— the answer is ready.status: "needs_input"— the assistant needsquestionsanswered first. Relay them to your user (or decide yourself — each carries arecommended_indexhint), then call this endpoint again with the sameconversation_idand ananswersarray. Nothing is guessed on your behalf.status: "unresolved"— the prompt asked to CHANGE the project rather than asking about it.reasonnames the code-proposal endpoint to use instead. Returned immediately, before any analysis is billed.
Two response modes off one endpoint:
mode: "sync"(default) — a blocking JSON body.mode: "stream"— a Server-Sent Events stream (status/token/outcome/done/error). A turn that ends on questions emitsoutcomewith them instead of prose. See the Streaming guide.
Requires the ai_explain permission. Platform coverage is the capability matrix in the API overview; where it says 422, the refusal lands before anything is metered.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
Idempotency-Key required | header | string | A unique key (e.g. a UUID) for this write. Retrying with the same key and body replays the original result without double-billing. |
Request body required
| Field | Type | Description |
|---|---|---|
prompt | string | The question to ask about the project. |
conversation_id | string | Returned by every response. Pass it back to answer questions or to continue the same line of enquiry with prior turns in context. |
answers | array<object> | Answers to the questions a prior turn returned. |
answers[].id | string | The id of the question being answered. |
answers[].selected_index | integer | Zero-based index into that question's options. |
answers[].free_text | string | A free-text answer, when none of the options fit. |
mode | string enum | sync returns a blocking JSON body. stream returns an SSE stream. |
{
"prompt": "What conditions must be true for the main conveyor to start?",
"mode": "sync"
}Responses
| Status | Description |
|---|---|
| 200 | For mode: "sync", the answer or the questions that must be answered first. For mode: "stream", a text/event-stream of SSE events (see the Streaming guide). |
| 400 | The request was malformed. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 409 | A request with this Idempotency-Key is still being processed (idempotency_in_progress), or the project's analysis artifacts are not built yet (project_not_ready) — the latter clears on its own, so retry once the project's analysis reaches "complete". |
| 422 | The request was well-formed but cannot be carried out. The error code is one of: - idempotency_conflict — this Idempotency-Key was already used with a different body. Use a fresh key for a new request. - vendor_unsupported — this endpoint does not cover the project's PLC platform (see the capability matrix in the API overview). Not retryable; suggestedAction names the supported alternative. |
| 429 | Per-key rate limit exceeded. |
Response · 200
status: "answer"
| Field | Type | Description |
|---|---|---|
status required | string enum | |
conversation_id required | string | — |
answer required | string | — |
citations required | array<object> | What the answer was read from — the locations the assistant actually looked at while producing it. Empty when the turn read nothing citable; never absent. |
citations[].location_kind | string enum | What kind of place path names. |
citations[].path | string | The location as the platform spells it — Main/Feed_Conveyor (Rockwell), PLC_1/DriveStatus (Siemens), FB_Valve.Open (CODESYS). Safe to pass back to a read endpoint. |
citations[].rung | integer | The rung within path, when the read was rung-level. |
citations[].station | string | On a production line, the station this location belongs to. |
citations[].project_id | string | Present only when the location is on a line SIBLING rather than the project you asked about. |
citations_total | integer | How many distinct locations the turn read, when the per-kind quotas or the cap shortened citations. ABSENT when citations is the complete list — so a shortened list never reads as all of it. |
usage required | object | Token counts for this call — the sum across every model call the turn made. Informational only — not a bill. |
usage.input_tokens | integer | — |
usage.output_tokens | integer | — |
status: "needs_input"
The turn stopped to ask. Answer the questions and call again with the same conversation_id. This is a normal outcome, not an error.
| Field | Type | Description |
|---|---|---|
status required | string enum | |
conversation_id required | string | — |
questions required | array<object> | — |
questions[].id | string | — |
questions[].question | string | — |
questions[].options | array<string> | 2–4 suggested answers. You may answer with free_text instead. |
questions[].recommended_index | integer | The assistant's own suggestion, or null if it has none. A hint only — it is never applied for you. |
questions[].why_it_matters | string | What the answer changes, so you can decide how to ask. |
usage required | object | Token counts for this call — the sum across every model call the turn made. Informational only — not a bill. |
usage.input_tokens | integer | — |
usage.output_tokens | integer | — |
status: "unresolved"
The request could not be served as asked. On proposeCode: nothing could be proposed — usually the request named something that isn't in the project, or described no anchorable change. On askProject: the prompt was a change request, and reason names the code-proposal endpoint.
| Field | Type | Description |
|---|---|---|
status required | string enum | |
conversation_id required | string | — |
reason required | string | What went wrong and what to try, in plain language. |
usage required | object | Token counts for this call — the sum across every model call the turn made. Informational only — not a bill. |
usage.input_tokens | integer | — |
usage.output_tokens | integer | — |
Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Propose a code change (plan, then author)
Ask the assistant to PRODUCE or MODIFY PLC logic. Proposes, never deploys: no step here creates a version. To persist a proposal, commit it with POST /projects/{id}/versions (the separate code_write scope). Requires the ai_generate permission.
This is a two-step flow, because a change should be reviewed before it is written:
1. Send a prompt. You get status: "plan" — what would change, step by step, with any risks. No code is authored yet. 2. Show the plan to your user. If they approve, call again with the same conversation_id and approve: true (optionally with an amendment describing a tweak). You get status: "code" with the proposed changes.
The plan you approve is the one the server recorded in step 1 — it is not re-sent by you, so the change that gets authored is exactly the one that was reviewed.
A turn can also return status: "needs_input" (answer the questions and call again, as with askProject) or status: "unresolved" (nothing could be proposed; reason says what to do).
Two response modes off one endpoint:
mode: "sync"(default) — a blocking JSON body.mode: "stream"— a Server-Sent Events stream (status/token/outcome/done/error). See the Streaming guide.
Platform coverage is the capability matrix in the API overview; where it says 422, the refusal lands before anything is metered.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
Idempotency-Key required | header | string | A unique key (e.g. a UUID) for this write. Retrying with the same key and body replays the original result without double-billing. |
Request body required
| Field | Type | Description |
|---|---|---|
prompt | string | What to generate or change (e.g. "Add a 5-second start-up delay timer"). |
target | object | Optional hint for which program/routine to target. Folded into the prompt as guidance; the assistant decides the final placement. |
target.program | string | — |
target.routine | string | — |
conversation_id | string | Returned by every response. Required to approve or to answer questions. |
approve | boolean | Author the code for the plan pending on conversation_id. The plan executed is the one the server recorded when it was proposed, so what gets authored is exactly what was reviewed. 404 if no plan is pending. |
amendment | string | An optional plain-language tweak applied while executing the approved plan (e.g. "use a latch instead of a seal-in"). |
answers | array<object> | Answers to the questions a prior turn returned. |
answers[].id | string | The id of the question being answered. |
answers[].selected_index | integer | Zero-based index into that question's options. |
answers[].free_text | string | A free-text answer, when none of the options fit. |
mode | string enum | sync returns a blocking JSON body. stream returns an SSE stream. |
{
"prompt": "Add a 5-second start-up delay timer before the conveyor enables.",
"target": {
"program": "MainProgram",
"routine": "ConveyorControl"
},
"mode": "sync"
}Responses
| Status | Description |
|---|---|
| 200 | For mode: "sync", the plan, the proposed changes, or the questions that must be answered first. For mode: "stream", a text/event-stream of SSE events (see the Streaming guide). |
| 400 | The request was malformed. |
| 401 | No valid credential, or the key was revoked. |
| 402 | The organization reached its self-set API spend limit for the period and chose to pause. An owner/admin can raise the limit or enable overage billing in Settings → Spending Controls. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The project was not found, or approve: true named a conversation with no plan awaiting approval (already approved, or never proposed). |
| 409 | A request with this Idempotency-Key is still being processed (idempotency_in_progress), or the project's analysis artifacts are not built yet (project_not_ready) — the latter clears on its own, so retry once the project's analysis reaches "complete". |
| 422 | The request was well-formed but cannot be carried out. The error code is one of: - idempotency_conflict — this Idempotency-Key was already used with a different body. Use a fresh key for a new request. - vendor_unsupported — this endpoint does not cover the project's PLC platform (see the capability matrix in the API overview). Not retryable; suggestedAction names the supported alternative. |
| 429 | Per-key rate limit exceeded. |
Response · 200
status: "plan"
What the assistant would change. Nothing has been authored yet — call again with approve: true to author it.
| Field | Type | Description |
|---|---|---|
status required | string enum | |
conversation_id required | string | — |
plan required | object | — |
plan.summary | string | — |
plan.steps | array<object> | — |
plan.rationale | string | The approach and design choices, in plain language. |
plan.assumptions | array<string> | Ambiguities the assistant resolved — worth a sanity check before approving. |
plan.affected_tags | array<string> | — |
plan.risks | array<object> | Uncertainty and blast-radius signals. Surface these to a human before approving. |
citations required | array<object> | The locations the assistant read while working out this plan. |
citations[].location_kind | string enum | What kind of place path names. |
citations[].path | string | The location as the platform spells it — Main/Feed_Conveyor (Rockwell), PLC_1/DriveStatus (Siemens), FB_Valve.Open (CODESYS). Safe to pass back to a read endpoint. |
citations[].rung | integer | The rung within path, when the read was rung-level. |
citations[].station | string | On a production line, the station this location belongs to. |
citations[].project_id | string | Present only when the location is on a line SIBLING rather than the project you asked about. |
citations_total | integer | How many distinct locations the turn read, when the per-kind quotas or the cap shortened citations. ABSENT when citations is the complete list — so a shortened list never reads as all of it. |
usage required | object | Token counts for this call — the sum across every model call the turn made. Informational only — not a bill. |
usage.input_tokens | integer | — |
usage.output_tokens | integer | — |
status: "code"
The authored proposal. This creates NO version — commit it with POST /projects/{id}/versions (code_write) to persist it.
| Field | Type | Description |
|---|---|---|
status required | string enum | |
conversation_id required | string | — |
explanation required | string | Plain-language summary of what was authored. |
changes required | array<object> | — |
changes[].action | string | e.g. CREATE, UPDATE, DELETE, RENAME, DUPLICATE. |
changes[].target | string | The unit's path, e.g. Program:Main/Routine:Feed or PLC_1/FB_Feeder. |
changes[].code_type | string | ST | RLL | Tag | UDT for Rockwell; SCL | DB | LAD | FBD | SFC | Tag | UDT for Siemens; ST | Declaration | LD | Task for CODESYS. |
changes[].content | string | The proposed source for target, with two CODESYS exceptions. On an LD change it is a rung-edit script (UPDATE_RUNG / INSERT_RUNG_AFTER / DELETE_RUNG lines against the numbered rungs of the existing body), because a CODESYS ladder body is edited rung by rung rather than replaced. On a Task change it is the program name to schedule, or the new task's fields. |
changes[].new_name | string | The new name, for a RENAME or the copy of a DUPLICATE. |
citations required | array<object> | The locations the assistant read while authoring these changes. |
citations[].location_kind | string enum | What kind of place path names. |
citations[].path | string | The location as the platform spells it — Main/Feed_Conveyor (Rockwell), PLC_1/DriveStatus (Siemens), FB_Valve.Open (CODESYS). Safe to pass back to a read endpoint. |
citations[].rung | integer | The rung within path, when the read was rung-level. |
citations[].station | string | On a production line, the station this location belongs to. |
citations[].project_id | string | Present only when the location is on a line SIBLING rather than the project you asked about. |
citations_total | integer | How many distinct locations the turn read, when the per-kind quotas or the cap shortened citations. ABSENT when citations is the complete list — so a shortened list never reads as all of it. |
usage required | object | Token counts for this call — the sum across every model call the turn made. Informational only — not a bill. |
usage.input_tokens | integer | — |
usage.output_tokens | integer | — |
status: "needs_input"
The turn stopped to ask. Answer the questions and call again with the same conversation_id. This is a normal outcome, not an error.
| Field | Type | Description |
|---|---|---|
status required | string enum | |
conversation_id required | string | — |
questions required | array<object> | — |
questions[].id | string | — |
questions[].question | string | — |
questions[].options | array<string> | 2–4 suggested answers. You may answer with free_text instead. |
questions[].recommended_index | integer | The assistant's own suggestion, or null if it has none. A hint only — it is never applied for you. |
questions[].why_it_matters | string | What the answer changes, so you can decide how to ask. |
usage required | object | Token counts for this call — the sum across every model call the turn made. Informational only — not a bill. |
usage.input_tokens | integer | — |
usage.output_tokens | integer | — |
status: "unresolved"
The request could not be served as asked. On proposeCode: nothing could be proposed — usually the request named something that isn't in the project, or described no anchorable change. On askProject: the prompt was a change request, and reason names the code-proposal endpoint.
| Field | Type | Description |
|---|---|---|
status required | string enum | |
conversation_id required | string | — |
reason required | string | What went wrong and what to try, in plain language. |
usage required | object | Token counts for this call — the sum across every model call the turn made. Informational only — not a bill. |
usage.input_tokens | integer | — |
usage.output_tokens | integer | — |
Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Create a troubleshooting thread
Open a stateful multi-turn thread scoped to a project. Append turns with POST /conversations/{cid}/messages. Requires ai_explain.
A thread answers each turn exactly as interpret does, so it covers the same platforms — see the capability matrix in the API overview.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
Idempotency-Key required | header | string | A unique key (e.g. a UUID) for this write. Retrying with the same key and body replays the original result without double-billing. |
Request body
| Field | Type | Description |
|---|---|---|
name | string | Optional human label for the thread. |
{
"name": "string"
}Responses
| Status | Description |
|---|---|
| 201 | The created thread. |
| 400 | The request was malformed. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 409 | A request with this Idempotency-Key is still being processed. |
| 422 | The request was well-formed but cannot be carried out. The error code is one of: - idempotency_conflict — this Idempotency-Key was already used with a different body. Use a fresh key for a new request. - vendor_unsupported — this endpoint does not cover the project's PLC platform (see the capability matrix in the API overview). Not retryable; suggestedAction names the supported alternative. |
| 429 | Per-key rate limit exceeded. |
Response · 201
| Field | Type | Description |
|---|---|---|
conversationId required | string | — |
projectId required | string | — |
{
"conversationId": "string",
"projectId": "string"
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Append a turn to a thread
Run one interpret turn inside an existing thread. The thread's prior history is loaded automatically. Requires ai_explain.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
cid required | path | string | The conversation (thread) id. |
Idempotency-Key required | header | string | A unique key (e.g. a UUID) for this write. Retrying with the same key and body replays the original result without double-billing. |
Request body required
| Field | Type | Description |
|---|---|---|
prompt | string | — |
answers | array<object> | Answers to the questions the previous turn on this thread returned. |
answers[].id | string | The id of the question being answered. |
answers[].selected_index | integer | Zero-based index into that question's options. |
answers[].free_text | string | A free-text answer, when none of the options fit. |
{
"prompt": "string",
"answers": [
{
"id": "string",
"selected_index": 0,
"free_text": "string"
}
]
}Responses
| Status | Description |
|---|---|
| 200 | The assistant's answer for this turn. |
| 400 | The request was malformed. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 409 | A request with this Idempotency-Key is still being processed. |
| 422 | This Idempotency-Key was already used with a different body. |
| 429 | Per-key rate limit exceeded. |
Response · 200
status: "answer"
| Field | Type | Description |
|---|---|---|
status required | string enum | |
conversation_id required | string | — |
answer required | string | — |
citations required | array<object> | What the answer was read from — the locations the assistant actually looked at while producing it. Empty when the turn read nothing citable; never absent. |
citations[].location_kind | string enum | What kind of place path names. |
citations[].path | string | The location as the platform spells it — Main/Feed_Conveyor (Rockwell), PLC_1/DriveStatus (Siemens), FB_Valve.Open (CODESYS). Safe to pass back to a read endpoint. |
citations[].rung | integer | The rung within path, when the read was rung-level. |
citations[].station | string | On a production line, the station this location belongs to. |
citations[].project_id | string | Present only when the location is on a line SIBLING rather than the project you asked about. |
citations_total | integer | How many distinct locations the turn read, when the per-kind quotas or the cap shortened citations. ABSENT when citations is the complete list — so a shortened list never reads as all of it. |
usage required | object | Token counts for this call — the sum across every model call the turn made. Informational only — not a bill. |
usage.input_tokens | integer | — |
usage.output_tokens | integer | — |
status: "needs_input"
The turn stopped to ask. Answer the questions and call again with the same conversation_id. This is a normal outcome, not an error.
| Field | Type | Description |
|---|---|---|
status required | string enum | |
conversation_id required | string | — |
questions required | array<object> | — |
questions[].id | string | — |
questions[].question | string | — |
questions[].options | array<string> | 2–4 suggested answers. You may answer with free_text instead. |
questions[].recommended_index | integer | The assistant's own suggestion, or null if it has none. A hint only — it is never applied for you. |
questions[].why_it_matters | string | What the answer changes, so you can decide how to ask. |
usage required | object | Token counts for this call — the sum across every model call the turn made. Informational only — not a bill. |
usage.input_tokens | integer | — |
usage.output_tokens | integer | — |
status: "unresolved"
The request could not be served as asked. On proposeCode: nothing could be proposed — usually the request named something that isn't in the project, or described no anchorable change. On askProject: the prompt was a change request, and reason names the code-proposal endpoint.
| Field | Type | Description |
|---|---|---|
status required | string enum | |
conversation_id required | string | — |
reason required | string | What went wrong and what to try, in plain language. |
usage required | object | Token counts for this call — the sum across every model call the turn made. Informational only — not a bill. |
usage.input_tokens | integer | — |
usage.output_tokens | integer | — |
Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Re-run the project's analysis
Run analysis again on the project's current version, and return immediately — the run continues in the background. Read the result with GET /projects/{id}/analysis.
This is the only verb on this surface that produces an analysis. A project uploaded in the app is analyzed there; a version committed with POST /projects/{id}/versions is not, so this is how a caller gets analysis onto a version it authored. It is a deliberate, billable act — analysis is expensive and per-version — which is why it is an HTTP operation rather than an agent tool.
Only one run exists per version at a time. If a healthy run is already in flight this joins it rather than scheduling a second: the response says started: false and no additional work (or spend) is incurred.
The run is not selectable: there is no request body, and every phase the project's detected industry calls for is executed. Which standards chapters appear therefore varies by project, not by request. Requires analysis_tab. Covers all three platforms.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
Idempotency-Key required | header | string | A unique key (e.g. a UUID) for this write. Retrying with the same key and body replays the original result without double-billing. |
Responses
| Status | Description |
|---|---|
| 202 | A run is in flight — either newly started, or already going. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 409 | A request with this Idempotency-Key is still being processed. |
| 422 | This Idempotency-Key was already used with a different body. |
| 429 | Per-key rate limit exceeded. |
Response · 202
| Field | Type | Description |
|---|---|---|
analysis_id required | string | Read this run back with GET /analyses/{analysisId}. |
project_id required | string | — |
version_id required | string | The version the run analyses — read the result for this id. |
status required | string enum | |
started required | boolean | True when this call started the run. False when a healthy run was already in flight and this call joined it without scheduling a second (and without additional spend). |
{
"analysis_id": "string",
"project_id": "string",
"version_id": "string",
"status": "queued",
"started": true
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Read the project's analysis
Return the analysis the platform already ran for this project — its current version by default, or the version version_id names. Nothing produces one as a side effect: it comes from analyzing the project in the app, or from an explicit POST /projects/{id}/analyses.
status is one of:
not_analyzed— no analysis has ever run for this version. It will not start on its own;POST /projects/{id}/analysesproduces one. This is a normal state, not an error, and it is the expected answer right afterPOST /projects/{id}/versions, which commits a version without analyzing it.queued/running— a run is in flight. The response carries amessagewith poll guidance and no results yet. Normal progress.complete—resultsholds the analysis.failed_phasesmay still be present when some phases failed but the run finished.error— the run failed;failed_phasesnames the phases that did.
When the current version is not_analyzed, the project's existing analysis is still reachable. The response then carries last_analyzed_version_id (and last_analyzed_at) naming the most recent version whose analysis completed — usually the version the last commit superseded. Pass that id back as ?version_id= to read it.
A version-pinned read serves that version's stored analysis and never substitutes it for the current one: the payload keeps naming the version it describes and carries is_current_version: false. Treat it as a statement about superseded code — report which version it describes and that the current version has not been analyzed, and remember only POST /projects/{id}/analyses produces a current one.
version_id resolves strictly inside the project in the path. A version belonging to another project — or another organization — returns 404, identical to an id that exists nowhere. A version that exists but was never analyzed is a normal 200 not_analyzed, not a 404.
For a cheap freshness check that returns no results blob, read analysis_status on GET /projects/{id} instead. Requires analysis_tab. Covers all three platforms.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
version_id | query | string | Read this version's stored analysis instead of the current version's. Must be a version of the project in the path. Omit for the current version. |
Responses
| Status | Description |
|---|---|
| 200 | The analysis of the version read (and results when complete). |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 429 | Per-key rate limit exceeded. |
Response · 200
| Field | Type | Description |
|---|---|---|
project_id required | string | — |
version_id required | string | Analysis is per-version; this is the version the results describe. |
is_current_version required | boolean | Whether version_id is still the project's current version. Always present, so a payload can be judged without knowing which call produced it. false means these findings describe code the project no longer contains. Analysis findings are specific claims about specific code ("missing handshake between Filler and Capper"), so a superseded one can be wrong in either direction — the commit that superseded this version may already have fixed the finding, or introduced one this run never saw. Report which version it describes; never present it as the project's current state. |
status required | string enum | |
analyzed_at | string (date-time) | When the run reached a terminal state. |
message | string | Present while queued/running — poll-interval guidance. |
results | object | The analysis, present when status is complete. |
failed_phases | array<string> | Names of the analysis phases that failed, if any. Present alongside complete when the failure was partial and results is still usable. Names only — failure messages are not published. |
last_analyzed_version_id | string | The most recent version of this project whose analysis completed. Present ONLY alongside status: not_analyzed on a read of the current version — the state a POST /projects/{id}/versions commit leaves behind. It means: an analysis exists for an older version. Fetch it deliberately with GET /projects/{id}/analysis?version_id=<this>, and label what you report from it — the response will carry is_current_version: false. Absent when the project has never had a completed analysis. Nothing here is a substitute for a current analysis; only POST /projects/{id}/analyses produces one. |
last_analyzed_at | string (date-time) | When that older run completed — what its staleness is judged against. |
{
"project_id": "string",
"version_id": "string",
"is_current_version": true,
"status": "not_analyzed",
"analyzed_at": "2026-05-31T18:15:00.000Z",
"message": "string",
"results": null,
"failed_phases": [
"string"
],
"last_analyzed_version_id": "string",
"last_analyzed_at": "2026-05-31T18:15:00.000Z"
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Read one analysis run
Return the outcome of a single analysis run, by the analysis_id startAnalysis gave you.
This stays pinned to the version the run analysed. If a new version is committed while the run is in flight, this still reports *your* run, while GET /projects/{id}/analysis moves on to the new current version. is_current_version tells you directly whether the version this run analysed is still the project's live one — when it is false, these findings describe code the project no longer contains and must not be reported as its current state.
Not a run history: a version keeps only its most recent run id, so starting a second run on the same version makes the older id return 404. Requires analysis_tab. Rate-limited on the slow-job tier — an analysis takes minutes, so poll no faster than every 5 seconds.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
analysisId required | path | string | The analysis run id returned by startAnalysis. |
Responses
| Status | Description |
|---|---|
| 200 | The run's status (and results when complete). |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 429 | Per-key rate limit exceeded. |
Response · 200
object
Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Export the project to its vendor PLC file
Start an async export of the project's current committed version to its native vendor format (a Rockwell .L5X, a Siemens TIA .zip or a CODESYS .export). Returns a job id immediately; poll GET /exports/{exportId} for status, then GET the download_url when complete. Exports the current version as-is (no edits). Requires export_plc.
A CODESYS export is written by patching the .export the version was uploaded from, so that file must still be in storage. When it is not, the job reaches status: "error" with a message saying so — the 202 only means the job was accepted, so always poll for the terminal state.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
Idempotency-Key required | header | string | A unique key (e.g. a UUID) for this write. Retrying with the same key and body replays the original result without double-billing. |
Responses
| Status | Description |
|---|---|
| 202 | The export job was started. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 409 | A request with this Idempotency-Key is still being processed. |
| 422 | This Idempotency-Key was already used with a different body. |
| 429 | Per-key rate limit exceeded. |
Response · 202
| Field | Type | Description |
|---|---|---|
exportId required | string | — |
status required | string enum |
{
"exportId": "string",
"status": "pending"
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Render a PDF report for the project
Start an async render of a PDF documentation report for the project's current committed version. The report is assembled server-side from the parsed structure + analysis results (a "report-only" PDF; it does not include in-app chat history). Returns a job id immediately; poll GET /exports/{exportId} then GET the download_url. Requires export_pdf.
Which chapters appear depends on the platform and on whether an analysis has completed — see *What a PDF report contains differs by platform* under the capability matrix. A report rendered before any analysis run contains the structural chapters only.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
Idempotency-Key required | header | string | A unique key (e.g. a UUID) for this write. Retrying with the same key and body replays the original result without double-billing. |
Responses
| Status | Description |
|---|---|
| 202 | The export job was started. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 409 | A request with this Idempotency-Key is still being processed. |
| 422 | This Idempotency-Key was already used with a different body. |
| 429 | Per-key rate limit exceeded. |
Response · 202
| Field | Type | Description |
|---|---|---|
exportId required | string | — |
status required | string enum |
{
"exportId": "string",
"status": "pending"
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Poll an export job
Return the current status of an export job. While pending/running the response carries a message with poll guidance. When complete, download_url (a relative /exports/{id}/download path) and filename are populated; when error, error carries a short reason. Gated on the export's matching permission (export_plc / export_pdf).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
exportId required | path | string | The export job id returned by startPlcExport / startPdfExport. |
Responses
| Status | Description |
|---|---|
| 200 | The export job status (and download_url when complete). |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 429 | Per-key rate limit exceeded. |
Response · 200
| Field | Type | Description |
|---|---|---|
exportId required | string | — |
kind required | string enum | |
status required | string enum | |
download_url | string | Present only when status is complete. A relative path. |
filename | string | Suggested download filename (present when complete). |
error | string | Short failure reason (present when status is error). |
message | string | Poll guidance (present while pending/running). |
{
"exportId": "string",
"kind": "plc",
"status": "pending",
"download_url": "string",
"filename": "string",
"error": "string",
"message": "string"
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Download a completed export artifact
Stream the finished artifact (L5X / Siemens ZIP / PDF) for a complete export job, with the right Content-Type and a Content-Disposition filename. Gated on the export's matching permission and scope.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
exportId required | path | string | The export job id returned by startPlcExport / startPdfExport. |
Responses
| Status | Description |
|---|---|
| 200 | The export artifact bytes. |
| 400 | The request was malformed. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 429 | Per-key rate limit exceeded. |
Every 4xx/5xx uses the standard error envelope and carries a request-id header.
List projects
Enumerate the organization's active projects, most-recently-touched first. Requires only a valid key (no extra permission) — an ai_explain-only operator key still needs to discover which projects exist. Results are filtered to the key's project scope. Returns identity-level metadata only; read source via GET /projects/{id}/source.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
team_id | query | string | Restrict to a single team. |
limit | query | integer | Page size (default 50, max 200). |
cursor | query | string | Opaque cursor from a previous response's next_cursor. |
Responses
| Status | Description |
|---|---|
| 200 | A page of projects. |
| 400 | The request was malformed. |
| 401 | No valid credential, or the key was revoked. |
| 429 | Per-key rate limit exceeded. |
Response · 200
| Field | Type | Description |
|---|---|---|
projects required | array<object> | — |
projects[].project_id | string | — |
projects[].identity_id | string | The stable project identity id that project scope is keyed on. |
projects[].name | string | — |
projects[].vendor | string enum | |
projects[].industry | string | The industry the project's last analysis run recorded. Three states, and the third is not the second: a named industry; "general", meaning the project was examined and matched no industry-specific patterns; or null, meaning it has never been analysed — which says nothing about its industry either way. |
projects[].file_type | string | e.g. "l5x", "zip", "scl". |
projects[].current_version_id | string | — |
projects[].updated_at | string (date-time) | — |
next_cursor required | string | Pass as ?cursor= to fetch the next page; null on the last page. |
{
"projects": [
{
"project_id": "string",
"identity_id": "string",
"name": "string",
"vendor": "allen-bradley",
"industry": "string",
"file_type": "string",
"current_version_id": "string",
"updated_at": "2026-05-31T18:15:00.000Z"
}
],
"next_cursor": "string"
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Get a project
Identity + current-version metadata for one project, plus an analysis_status summary (not the full results blob). Requires only a valid key (no extra permission); scoped to the key's project scope. Unknown or out-of-scope id → 404.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
Responses
| Status | Description |
|---|---|
| 200 | The project metadata. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 429 | Per-key rate limit exceeded. |
Response · 200
| Field | Type | Description |
|---|---|---|
project_id required | string | — |
identity_id required | string | — |
name required | string | — |
display_name | string | — |
vendor | string | — |
industry | string | The industry the project's last analysis run recorded. Three states, and the third is not the second: a named industry; "general", meaning the project was examined and matched no industry-specific patterns; or null, meaning it has never been analysed — which says nothing about its industry either way. |
file_type | string | — |
file_size_bytes | integer | — |
original_filename | string | — |
current_version_id required | string | — |
version_number | integer | — |
analysis_status | string enum | Summary only — poll GET /analyses/{id} for full results. null = never analyzed. |
created_at required | string (date-time) | — |
updated_at required | string (date-time) | — |
{
"project_id": "string",
"identity_id": "string",
"name": "string",
"display_name": "string",
"vendor": "string",
"industry": "string",
"file_type": "string",
"file_size_bytes": 0,
"original_filename": "string",
"current_version_id": "string",
"version_number": 0,
"analysis_status": "queued",
"created_at": "2026-05-31T18:15:00.000Z",
"updated_at": "2026-05-31T18:15:00.000Z"
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Commit a new version (Save project)
Save a new version of an existing project from an updated vendor file — the headless equivalent of the platform's "Save project". The new version becomes the project's current version (billed identically to a UI save). Requires code_write.
It does not analyze. A commit is an authored edit, and analysis is expensive and per-version, so the new version starts out unanalyzed: the 201 carries analysis: "not_analyzed", and GET /projects/{id}/analysis reports the same until someone asks for a run with POST /projects/{id}/analyses. This matches the app, where the editor's Save does not analyze and Update Analysis is a separate action. Graph and search indexing DO run, so questions asked through interpret / conversations see the committed change immediately.
The uploaded file must be the same vendor as the project (an L5X stays L5X, a Siemens ZIP stays Siemens, a CODESYS .export stays .export) — a mismatch returns 400. Re-sending the exact bytes of the current version is a no-op: it returns 200 with resolution: identical_file and no new version.
Files up to ~4.5 MB may be sent inline as base64 in file.inline; larger files use the large-file flow via file.blob_url. A project-scoped key may only commit to in-scope projects.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
Idempotency-Key required | header | string | A unique key (e.g. a UUID) for this write. Retrying with the same key and body replays the original result without double-billing. |
Request body required
| Field | Type | Description |
|---|---|---|
originalFilename required | string | The export file name — its extension must match the project's vendor, e.g. "Injection_Molding.L5X", "Reference_Coiler.zip" or "AnalogueClock.export". |
name | string | Optional display name for the project. |
file required | object | Exactly one of inline or blob_url must be set. |
file.inline | string | Base64-encoded file bytes (for files up to ~4.5 MB). |
file.blob_url | string (uri) | A blob URL for the large-file flow. |
{
"originalFilename": "string",
"name": "string",
"file": {
"inline": "string",
"blob_url": "https://…"
}
}Responses
| Status | Description |
|---|---|
| 200 | The uploaded bytes matched the current version; no new version was created. |
| 201 | A new version was committed. |
| 400 | The request was malformed. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 409 | A request with this Idempotency-Key is still being processed. |
| 413 | The inline body exceeded the limit; use the large-file flow. |
| 422 | This Idempotency-Key was already used with a different body. |
| 429 | Per-key rate limit exceeded. |
Response · 200
| Field | Type | Description |
|---|---|---|
project_id required | string | — |
identity_id required | string | — |
version_id | string | The new version's id; null when resolution is identical_file. |
version_number required | integer | — |
resolution required | string enum | add_version when a new version was committed; identical_file when the uploaded bytes matched the current version (no-op). |
vendor required | string enum | |
analysis | string enum | The new version's analysis state — always not_analyzed, because a commit does not analyze. Ask for a run with POST /projects/{id}/analyses. Absent when resolution is identical_file: no new version was written, so the project's existing analysis still describes its current one. |
{
"project_id": "string",
"identity_id": "string",
"version_id": "string",
"version_number": 0,
"resolution": "add_version",
"vendor": "allen-bradley",
"analysis": "not_analyzed"
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Read a project's source
Return the project itself, in one of two representations. Requires code_read. There is deliberately nothing between them: the API serves whole-project reads and reasoning verbs, not fine-grained retrieval.
format=parsed(default): the vendor-neutral parsed model as JSON — the same structure for all three platforms. Served straight from the stored artifact, so the response body is the parsed model, not an envelope around it. There is no size ceiling: the payload is never buffered into the response.format=raw: the current committed version's vendor file (L5X XML, Siemens ZIP or CODESYS.export), as a binary download. For a version saved in-app (where only the parsed model carries the edit) this is regenerated from the parsed model — the same bytes a PLC export produces — so it always reflects the latest committed state, not the originally uploaded file.
Both are served by streaming the stored object, or by a 302 to a short-lived storage URL where the backend offers one. Follow redirects.
Compression. Stored payloads are gzipped, and serving them that way is preferred — send Accept-Encoding: gzip and a parsed model arrives roughly 14x smaller. The response is then Content-Encoding: gzip; any client that advertises gzip inflates it transparently. A client that does NOT advertise gzip is served the decompressed bytes instead, so a plain request always yields directly usable content.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
format | query | string enum | — |
Responses
| Status | Description |
|---|---|
| 200 | The requested representation: the parsed model as JSON for parsed, a binary stream for raw. Content-Encoding: gzip when the request advertised gzip. |
| 302 | The payload lives at a short-lived storage URL given in Location. Follow it to read the bytes; most HTTP clients do so automatically. |
| 400 | The request was malformed. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 429 | Per-key rate limit exceeded. |
Response · 200
object
{}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Latest live tag values
The most recent tag snapshot pushed by a Desktop Companion App (DCA) session for this project. Requires hmi_view. Returns data only while a DCA is actively streaming; with no live session, live is false and tags is empty (this is a normal 200, not an error).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
Responses
| Status | Description |
|---|---|
| 200 | The latest snapshot, or an empty live=false body. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 429 | Per-key rate limit exceeded. |
Response · 200
| Field | Type | Description |
|---|---|---|
project_id required | string | — |
live required | boolean | True when a DCA session is actively streaming this project. |
message | string | Present when live=false — explains why there are no values. |
device_id | string | — |
timestamp | string (date-time) | — |
controller | object | — |
controller.name | string | — |
controller.type | string | — |
controller.status | string | — |
controller.ipAddress | string | — |
sequence | integer | — |
tags required | map<string, object> | Map of tag name → latest value. |
{
"project_id": "string",
"live": true,
"message": "string",
"device_id": "string",
"timestamp": "2026-05-31T18:15:00.000Z",
"controller": {
"name": "string",
"type": "string",
"status": "string",
"ipAddress": "string"
},
"sequence": 0,
"tags": {
"example_permission": null
}
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Recent value history for a tag
The most-recent-first value history a DCA session pushed for one tag. Requires hmi_view and a tag query parameter. Empty when no live session has streamed that tag.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
tag required | query | string | The tag name to fetch history for. |
limit | query | integer | Max entries (default 50, max 600). |
Responses
| Status | Description |
|---|---|
| 200 | The tag's recent history. |
| 400 | The request was malformed. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 429 | Per-key rate limit exceeded. |
Response · 200
| Field | Type | Description |
|---|---|---|
project_id required | string | — |
tag required | string | — |
count required | integer | — |
history required | array<object> | Most-recent-first history entries. |
history[].value | object | — |
history[].timestamp | string (date-time) | — |
history[].sequence | integer | — |
{
"project_id": "string",
"tag": "string",
"count": 0,
"history": [
{
"value": null,
"timestamp": "2026-05-31T18:15:00.000Z",
"sequence": 0
}
]
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Request live values for specific tags on demand
Ask the Desktop Companion App (DCA) serving this project to read a specific set of tags on its next poll — even if the operator never pressed "Start streaming" (the DCA only needs: connected + signed in + project selected). Requires hmi_edit.
This is a thin request signal, not a data read: it records the requested tag names and reports whether a DCA is currently able to serve them (live). Poll GET /projects/{id}/hmi/values (or interpret) a few seconds later to read the delivered values.
Naturally idempotent — re-requesting the same tags simply extends the request window — so no Idempotency-Key is required. The list is de-duped and never truncated, and maxItems declares the per-request ceiling, so a whole-project tag set can be requested in one call; unknown names are dropped by the DCA against its discovered tag list.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The project id. |
Request body required
| Field | Type | Description |
|---|---|---|
tags required | array<string> | Tag names to request live values for (verbatim project tag names). De-duped, and never truncated — ask for every tag in a project and every tag is recorded. maxItems is the declared per-request ceiling (the per-message transport shape), so a whole-project tag set fits in one request; unknown names are dropped by the DCA. |
{
"tags": [
"string"
]
}Responses
| Status | Description |
|---|---|
| 200 | The request was recorded; live reports DCA availability. |
| 400 | The request was malformed. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 429 | Per-key rate limit exceeded. |
| 503 | The endpoint is gated by a server feature flag that is currently off. |
Response · 200
| Field | Type | Description |
|---|---|---|
project_id required | string | — |
live required | boolean | True when a DCA is currently able to serve this project (connected + project selected, or already streaming). When false, the request is still recorded but no values arrive until a DCA connects. |
requested required | array<string> | The tag names actually recorded (de-duped; empties dropped). |
{
"project_id": "string",
"live": true,
"requested": [
"string"
]
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Mint a read-only embed token
Mint a short-lived, project-scoped token for the embeddable read-only assistant iframe. Regardless of the minting key's scope, the token is intersected down to read-only (ai_explain + hmi_view) — a browser-delivered token can never carry a write scope. The minting key must itself have at least ai_explain.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
Idempotency-Key required | header | string | A unique key (e.g. a UUID) for this write. Retrying with the same key and body replays the original result without double-billing. |
Request body required
| Field | Type | Description |
|---|---|---|
project_id required | string | The project the embed token may access. |
{
"project_id": "string"
}Responses
| Status | Description |
|---|---|
| 201 | The minted read-only embed token. |
| 400 | The request was malformed. |
| 401 | No valid credential, or the key was revoked. |
| 403 | The key lacks the permission (or project scope) this endpoint needs. |
| 404 | The resource was not found, or is not visible to this key's org. |
| 409 | A request with this Idempotency-Key is still being processed. |
| 422 | This Idempotency-Key was already used with a different body. |
| 429 | Per-key rate limit exceeded. |
Response · 201
| Field | Type | Description |
|---|---|---|
token required | string | The signed, read-only embed token to hand to a browser. |
expires_at required | string (date-time) | — |
permissions required | map<string, boolean> | The intersected read-only permissions (only ai_explain / hmi_view). |
project_id required | string | — |
{
"token": "string",
"expires_at": "2026-05-31T18:15:00.000Z",
"permissions": {
"example_permission": true
},
"project_id": "string"
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.