Reference
API reference.
A stable, versioned HTTP API for interpreting and analyzing PLC projects.
The PLCs.ai API exposes the same engine that powers PLCs.ai — plain-language interpretation, troubleshooting, and analysis of Allen-Bradley and Siemens projects — behind a versioned, externally-authenticated HTTP surface.
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 and the route-conformance check 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.
Interpret a prompt against a project
The headline capability: a prompt in, a cited interpretation out, over the existing assistant. Two response modes off one endpoint:
mode: "sync"(default) — a blocking JSON body{ answer, citations, usage }.mode: "stream"— a Server-Sent Events stream (status/token/citations/done/error). See the Streaming guide.
Requires the ai_explain permission.
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 required | string | The question to ask about the project. |
mode | string enum | sync returns a blocking JSON body. stream returns an SSE stream. |
include_citations | boolean | Whether to include source citations in the response. |
{
"prompt": "What conditions must be true for the main conveyor to start?",
"mode": "sync",
"include_citations": true
}Responses
| Status | Description |
|---|---|
| 200 | For mode: "sync", the cited interpretation. For mode: "stream", an 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. |
| 422 | This Idempotency-Key was already used with a different body. |
| 429 | Per-key rate limit exceeded. |
Response · 200
| Field | Type | Description |
|---|---|---|
answer required | string | — |
citations required | array<string> | Source identifiers backing the answer. |
usage required | object | Token counts for this call. Informational only — not a bill. |
usage.input_tokens | integer | — |
usage.output_tokens | integer | — |
{
"answer": "string",
"citations": [
"string"
],
"usage": {
"input_tokens": 0,
"output_tokens": 0
}
}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.
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 | This Idempotency-Key was already used with a different body. |
| 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 required | string | — |
include_citations | boolean |
{
"prompt": "string",
"include_citations": true
}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
| Field | Type | Description |
|---|---|---|
conversationId required | string | — |
answer required | string | — |
citations required | array<string> | — |
usage required | object | Token counts for this call. Informational only — not a bill. |
usage.input_tokens | integer | — |
usage.output_tokens | integer | — |
{
"conversationId": "string",
"answer": "string",
"citations": [
"string"
],
"usage": {
"input_tokens": 0,
"output_tokens": 0
}
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Start an async analysis job
Kick off an analysis run (dead code, missing handshakes, cycle-time) and return a job id immediately. Poll GET /analyses/{analysisId} for status and results. Requires analysis_tab.
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 analysis job was started (or joined an in-flight run). |
| 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 |
|---|---|---|
analysisId required | string | — |
status required | string enum |
{
"analysisId": "string",
"status": "queued"
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Poll an analysis job
Return the current status of an analysis job. While running, the response carries a status of queued/running plus a message with poll guidance — never an empty 200. When complete, results is populated; when error, errors is populated. Requires analysis_tab.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
analysisId required | path | string | The analysis job id returned by startAnalysis. |
Responses
| Status | Description |
|---|---|
| 200 | The analysis job 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
| Field | Type | Description |
|---|---|---|
analysisId required | string | — |
status required | string enum | |
message | string | Present while queued/running — poll-interval guidance. |
results | object | Present when status is complete. |
errors | object | Present when status is error. |
{
"analysisId": "string",
"status": "queued",
"message": "string",
"results": null,
"errors": null
}Every 4xx/5xx uses the standard error envelope and carries a request-id header.
Ingest an L5X / Siemens export
Upload an L5X (Rockwell) or Siemens TIA ZIP export. The file is parsed server-side, a ProjectIdentity is resolved or created, and analysis + indexing are kicked off. Billed identically to a UI upload.
Files up to ~4.5 MB may be sent inline as base64 in file.inline. Larger files use the large-file flow: upload to the returned blob URL and pass file.blob_url. Requires project_upload.
A project-scoped key may only add a version to an in-scope identity; it can never create a new identity.
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 |
|---|---|---|
originalFilename required | string | The export file name, e.g. "Conveyor.L5X" or "TIA_Export.zip". |
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 |
|---|---|
| 201 | The resolved project identity and version. |
| 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. |
| 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 · 201
| Field | Type | Description |
|---|---|---|
identity_id required | string | — |
display_name | string | — |
project_id required | string | — |
version_id | string | — |
version_number | integer | — |
resolution required | string | How the identity was resolved (e.g. created / existing / identical_file). |
vendor required | string enum |
{
"identity_id": "string",
"display_name": "string",
"project_id": "string",
"version_id": "string",
"version_number": 0,
"resolution": "string",
"vendor": "allen-bradley"
}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.