This page takes an AI agent from nothing to a first write, using only an
mvk_ API key and the domain muviabi.com. Every request below is complete,
and so is every response (long lists are shortened to one or two items). Read it
top to bottom once; you will not have to guess.
The same text is available as Markdown at
https://developers.muviabi.com/guides/agent-quickstart.md, and
https://developers.muviabi.com/llms.txt indexes every guide.
| URL | |
|---|---|
| REST base URL | https://api.muviabi.com/api/public |
| MCP URL | https://api.muviabi.com/mcp/v1 |
Keep the key in an environment variable and never paste it into code, logs or prompts:
export MUVIA_API_KEY="mvk_<key_id>_<secret>"
The REST API is read-only and returns a company's projects, datasets and rows.
Any valid key answers, whatever its scopes. It counts toward the key's per-minute limit but not toward the company's daily quota, so call it first and whenever you are unsure what the key allows.
curl -s -H "X-Api-Key: $MUVIA_API_KEY" https://api.muviabi.com/api/public/v1/whoami
{
"key": {
"id": "k7Qp2xVd",
"name": "Reporting agent",
"scopes": ["settings_read", "datasets_read", "nodes_read"],
"expires_at": "2027-01-15T00:00:00Z"
},
"company": { "id": "6aa95e431b5b18b1850a2a21", "name": "ACME srl" },
"limits": {
"requests_per_minute_per_key": 120,
"requests_per_day_per_company": 100000,
"requests_today_company": 412,
"rows_page_size_max": 1000,
"page_size_max": 100
},
"mcp": { "url": "https://api.muviabi.com/mcp/v1", "available": true, "writes": false },
"links": {
"docs": "https://developers.muviabi.com",
"llms_txt": "https://developers.muviabi.com/llms.txt",
"openapi": "https://api.muviabi.com/api/public/openapi.json"
}
}
Needs settings_read.
curl -s -H "X-Api-Key: $MUVIA_API_KEY" https://api.muviabi.com/api/public/v1/company
{ "id": "6aa95e431b5b18b1850a2a21", "name": "ACME srl" }
mcp.writes in whoami says whether this key may propose or make changes
through MCP: true once an administrator has turned on «Can write through
assistants» on the key and it holds a permission a change needs. REST itself
only reads. Even a key that writes always proposes some changes, for a person
to confirm: analyses, links, annotations, a new project (see
Writes by a key).
Needs settings_read.
curl -s -H "X-Api-Key: $MUVIA_API_KEY" "https://api.muviabi.com/api/public/v1/projects?size=2"
{
"items": [
{
"id": "6abaad91a7a10614c38f70d5",
"name": "Energy Dome Demo",
"description": "Simulated CO2 battery plant",
"is_default": false,
"created_at": "2026-09-28T18:51:14.352000",
"updated_at": "2026-10-12T12:41:19.085000"
},
{
"id": "6aa95e431b5b18b1850a2a22",
"name": "Project 1",
"description": null,
"is_default": true,
"created_at": "2026-09-01T09:00:00.000000",
"updated_at": "2026-09-01T09:00:00.000000"
}
],
"total": 29,
"page": 1,
"size": 2,
"pages": 15
}
Needs datasets_read. Note each dataset's id and its columns.
curl -s -H "X-Api-Key: $MUVIA_API_KEY" \
"https://api.muviabi.com/api/public/v1/projects/6abaad91a7a10614c38f70d5/datasets?size=1"
{
"items": [
{
"id": "6abab724a7a10614c38f731c",
"name": "Compressor stops (10 min or more)",
"description": "Every period in which the compression train is stopped, with start and duration.",
"project_id": "6abaad91a7a10614c38f70d5",
"is_physical": false,
"columns": [
{ "name": "stop_id", "type": "Int64" },
{ "name": "started_at", "type": "DateTime('Europe/Rome')" },
{ "name": "duration_seconds", "type": "Int64" }
],
"created_at": "2026-09-28T18:51:09.626000",
"updated_at": "2026-10-12T12:41:19.444000"
}
],
"total": 14,
"page": 1,
"size": 1,
"pages": 14
}
Needs datasets_read. Rows are paged with page and size (up to 1000); keep
reading while has_more is true. See Pagination.
curl -s -H "X-Api-Key: $MUVIA_API_KEY" \
"https://api.muviabi.com/api/public/v1/projects/6abaad91a7a10614c38f70d5/datasets/6abab724a7a10614c38f731c/rows?size=2"
{
"columns": ["stop_id", "started_at", "duration_seconds"],
"items": [
{ "stop_id": 2, "started_at": "2026-09-29T01:14:02.164000", "duration_seconds": 21600 },
{ "stop_id": 64, "started_at": "2026-10-14T06:18:50.258000", "duration_seconds": 21600 }
],
"page": 1,
"size": 2,
"has_more": true
}
Every REST response carries X-RateLimit-Limit, X-RateLimit-Remaining and
X-RateLimit-Reset (see Rate limits). If anything fails, the answer is
the error envelope, with a hint that says what to do next.
REST has no query endpoint and no series endpoint. The rows endpoint pages
a dataset oldest first, so it is the wrong tool for "the latest values" or
"a time range". To read a time range, or to run any query, use MCP: call
describe_sql, then query_sql (step 11). For example, the last
day of a dataset, newest first:
SELECT started_at, duration_seconds
FROM `Compressor stops (10 min or more)_view`
WHERE started_at >= now() - INTERVAL 1 DAY
ORDER BY started_at DESC
LIMIT 100
MCP gives an agent far more than REST: hundreds of tools, a SQL query tool and (with permission) writes. See MCP server for the model; here is the wire exchange. MCP speaks JSON-RPC over HTTP POST. Every request carries these headers:
Authorization: Bearer $MUVIA_API_KEY
Content-Type: application/json
Accept: application/json, text/event-stream
The server answers as a server-sent event: one event: message line, then one
data: {...} line holding the JSON-RPC response. The examples show the JSON
only.
curl -s https://api.muviabi.com/mcp/v1 \
-H "Authorization: Bearer $MUVIA_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}'
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"logging": {},
"prompts": { "listChanged": true },
"resources": { "subscribe": false, "listChanged": true },
"tools": { "listChanged": true }
},
"serverInfo": { "name": "muvia", "version": "3.4.7" },
"instructions": "Muvia is an industrial data platform. Tools act as the connected person, in the company they connected, within the scopes they granted.\nCall whoami first, then list_projects: every other tool acts inside one project, whose time zone and rules describe_project tells.\nfind_tools names the tools of an area (datasets, signals, nodes, alarms, dashboards, analyses, reports, documents, predictors) when your client cannot search its tool list.\nPrefer the curated reads; use query_sql, after describe_sql, only when they are not enough."
}
}
Then tell the server you are ready. It answers 202 with no body:
curl -s -o /dev/null -w "%{http_code}\n" https://api.muviabi.com/mcp/v1 \
-H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
curl -s https://api.muviabi.com/mcp/v1 \
-H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
The answer lists every tool with its name, description, inputSchema and
annotations. Two entries, shortened:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "whoami",
"title": "Who am I connected as",
"description": "Who this connection acts as in Muvia: the person or API key, the company, the scopes granted and the client. Call it first to know what the other tools can do.",
"inputSchema": { "type": "object", "properties": {}, "additionalProperties": false },
"annotations": { "readOnlyHint": true, "openWorldHint": false }
},
{
"name": "annotate_signal",
"description": "Propose a note on the signals over a stretch of time, among the project's annotations; a person confirms it through approve_url unless the company applies it.",
"inputSchema": {
"type": "object",
"properties": {
"project_id": { "type": "string" },
"signals": { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 12 },
"start": { "type": "string", "format": "date-time" },
"end": { "type": "string", "format": "date-time" },
"text": { "type": "string", "minLength": 1, "maxLength": 2000 },
"label": { "type": "string", "enum": ["event", "maintenance", "anomaly", "note"] }
},
"required": ["project_id", "signals", "start", "end", "text"],
"additionalProperties": false
},
"annotations": { "destructiveHint": false, "openWorldHint": false }
}
]
}
}
readOnlyHint: true marks a read. A tool without it is a write (see the next
steps). destructiveHint: true marks the writes that delete or cannot be
undone. Every tool, with its arguments, is also listed for reading on
MCP tools (as Markdown at https://developers.muviabi.com/mcp-tools.md).
curl -s https://api.muviabi.com/mcp/v1 \
-H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'
A tool answer carries the result twice: as text in content and as an object in
structuredContent. Read structuredContent.
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [{ "type": "text", "text": "{\"kind\":\"api_key\", ...}" }],
"structuredContent": {
"kind": "api_key",
"company": { "id": "6aa95e431b5b18b1850a2a21", "name": "ACME srl" },
"user": null,
"client": null,
"scopes": ["datasets_read", "settings_read", "analysis_annotate"],
"permissions": ["datasets_read", "settings_read", "analysis_annotate"],
"key": { "name": "Reporting agent", "created_by": "Anna Rossi" },
"writes": true
},
"isError": false
}
}
writes is a plain boolean: true when an administrator has turned on «Can
write through assistants» on this key. For a key, key holds its name and
created_by. See MCP server.
curl -s https://api.muviabi.com/mcp/v1 \
-H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"list_projects","arguments":{}}}'
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"structuredContent": {
"columns": ["id", "name", "description"],
"rows": [
["6abaad91a7a10614c38f70d5", "Energy Dome Demo", "Simulated CO2 battery plant"],
["6aa95e431b5b18b1850a2a22", "Project 1", null]
]
},
"isError": false
}
}
Tables come back as columns plus rows (arrays in column order).
Call describe_sql first to learn which tables the project's SQL can read; it
answers {"columns":["name","read_in_sql_as"],"rows":[...]}, and the second
column is the name to write in FROM, exactly as given and in backticks: a
dataset is usually its name, character for character, plus _view
(Compressor stops (10 min or more)_view), a source a name of its own.
describe_sql is the only source of table names. Then run one SELECT:
curl -s https://api.muviabi.com/mcp/v1 \
-H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"query_sql","arguments":{"project_id":"6abaad91a7a10614c38f70d5","sql":"SELECT stop_id, started_at, duration_seconds FROM `Compressor stops (10 min or more)_view` ORDER BY started_at LIMIT 2","limit":2}}}'
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"structuredContent": {
"columns": ["stop_id", "started_at", "duration_seconds"],
"rows": [
[2, "2026-09-29T03:14:02+02:00", 21600],
[4, "2026-09-29T15:14:02+02:00", 21600]
],
"truncated": false,
"zone": "Europe/Rome"
},
"isError": false
}
}
To read a time range, put a WHERE on the time column and ORDER BY it
DESC with a LIMIT, as in step 6. truncated: true means more rows matched than limit returned. zone is the
project's time zone, in which instants are written. query_sql only ever runs
a read-only SELECT.
Writes use the same tools/call. Here the agent asks Muvia to note an event on
a signal:
curl -s https://api.muviabi.com/mcp/v1 \
-H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"annotate_signal","arguments":{"project_id":"6abaad91a7a10614c38f70d5","signals":["compressor_power"],"start":"2026-09-29T01:14:02Z","end":"2026-09-29T07:14:02Z","text":"Planned stop for filter change","label":"maintenance"}}}'
What comes back depends on the company's policy for this kind of change, and on the key (see MCP server).
Awaiting confirmation («Ask for confirmation», the usual policy). Nothing
has changed yet; a person approves the proposal in Muvia by opening
approve_url:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"structuredContent": {
"status": "pending",
"action_id": "6ad9f3c24e5b8a1d7c0f2b19",
"approve_url": "https://app.muviabi.com/approvals/6ad9f3c24e5b8a1d7c0f2b19?project=6abaad91a7a10614c38f70d5",
"summary": "Add a maintenance note to 1 signal, 29 Sep 2026 03:14 to 09:14",
"needs_admin": false
},
"isError": false
}
}
Applied at once («Run immediately»):
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"structuredContent": {
"status": "applied",
"action_id": "6ad9f3c24e5b8a1d7c0f2b19",
"id": "6ad9f3c24e5b8a1d7c0f2b1a",
"url": "https://app.muviabi.com/analysis?project=6abaad91a7a10614c38f70d5"
},
"isError": false
}
}
Calling the same write again within seconds returns the same answer instead of writing twice.
Both tools are writes, so both answer with a proposal or an applied change (see below). First find the ids they need.
Signal ids: list_signals returns one row per signal; the id is what
create_analysis takes. Signal ids are long and structured
(tag:<node>:<source>:<name>); copy them exactly.
curl -s https://api.muviabi.com/mcp/v1 \
-H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"list_signals","arguments":{"project_id":"6abaad91a7a10614c38f70d5","limit":3}}}'
{
"columns": ["id", "name", "unit", "last_value", "last_at", "untrackable_reason"],
"rows": [
["tag:16e0bd33-a313-4009-8af7-002a5be938b2:a05b0667-1e81-4745-a25f-3537eb600fad:AMB_T_C", "AMB_T_C", null, 22.67, "2026-10-19T21:05:34+00:00", null],
["tag:16e0bd33-a313-4009-8af7-002a5be938b2:a05b0667-1e81-4745-a25f-3537eb600fad:AUX_P_MW", "AUX_P_MW", null, 0.87, "2026-10-19T21:05:34+00:00", null]
],
"truncated": true,
"next_offset": 3
}
create_analysis. Input schema, from tools/list: project_id and title
(1 to 120 characters) are required; signals (1 to 12 ids from list_signals),
start and end (ISO 8601) give one trend; or definition gives a full
analysis in the shape get_analysis returns; optional description (up to 2000
characters) and labels (up to 10).
{
"jsonrpc": "2.0",
"id": 10,
"method": "tools/call",
"params": {
"name": "create_analysis",
"arguments": {
"project_id": "6abaad91a7a10614c38f70d5",
"title": "Ambient temperature and auxiliary power",
"signals": [
"tag:16e0bd33-a313-4009-8af7-002a5be938b2:a05b0667-1e81-4745-a25f-3537eb600fad:AMB_T_C",
"tag:16e0bd33-a313-4009-8af7-002a5be938b2:a05b0667-1e81-4745-a25f-3537eb600fad:AUX_P_MW"
],
"start": "2026-10-12T00:00:00Z",
"end": "2026-10-19T00:00:00Z",
"description": "Last week, one trend"
}
}
}
create_dashboard. project_id, name and widgets are required. Each
widget is bound to a dataset's columns (never to SQL of its own):
{title, type, dataset_id, channels, chart_kind?, reduce?}, where type is one
of CHART, NUMBER, LABEL, ALARM, ENUM, TABLE, dataset_id comes from
list_datasets, and channels maps each channel the type reads (x and y for
a line chart) to column names of that dataset.
{
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "create_dashboard",
"arguments": {
"project_id": "6abaad91a7a10614c38f70d5",
"name": "Daily charge energy",
"widgets": [
{
"title": "Energy absorbed per day",
"type": "CHART",
"chart_kind": "line",
"dataset_id": "6abab71da7a10614c38f72f0",
"channels": { "x": "bucket", "y": ["consumption"] }
}
]
}
}
}
Outcome 1: a proposal awaiting confirmation (the usual policy). Nothing is
created yet; a person opens approve_url in Muvia and confirms. The change is
then applied as that person.
{
"jsonrpc": "2.0",
"id": 11,
"result": {
"structuredContent": {
"status": "pending",
"action_id": "6ad9f4a04e5b8a1d7c0f2c31",
"approve_url": "https://app.muviabi.com/approvals/6ad9f4a04e5b8a1d7c0f2c31?project=6abaad91a7a10614c38f70d5",
"summary": "Create the dashboard \"Daily charge energy\" with 1 widget",
"needs_admin": false
},
"isError": false
}
}
Outcome 2: applied at once (the company's policy is «Run immediately»):
{
"jsonrpc": "2.0",
"id": 11,
"result": {
"structuredContent": {
"status": "applied",
"action_id": "6ad9f4a04e5b8a1d7c0f2c31",
"id": "6ad9f4a04e5b8a1d7c0f2c32",
"url": "https://app.muviabi.com/dashboards/6ad9f4a04e5b8a1d7c0f2c32?project=6abaad91a7a10614c38f70d5"
},
"isError": false
}
}
A change made by a key is shown in Muvia as authored by «API key» and the key's
name. create_analysis called with a key always answers with Outcome 1, whatever
the policy: an analysis belongs to a person, so the person who confirms it
creates and owns it (see MCP server). Through a
person's OAuth assistant it answers in either shape, like create_dashboard.
Ask later what a proposal turned into. status is pending, applied,
discarded, stale, expired or failed; an applied action carries its
result, a failed one a reason.
curl -s https://api.muviabi.com/mcp/v1 \
-H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"get_action","arguments":{"action_id":"6ad9f3c24e5b8a1d7c0f2b19"}}}'
{
"jsonrpc": "2.0",
"id": 8,
"result": {
"structuredContent": {
"status": "applied",
"result": {
"id": "6ad9f3c24e5b8a1d7c0f2b1a",
"url": "https://app.muviabi.com/analysis?project=6abaad91a7a10614c38f70d5"
}
},
"isError": false
}
}
A key that may not write gets a refusal instead, with isError: true and the
text «This API key can only read. To propose or make changes through an
assistant, an administrator must turn on «Can write through assistants» on the
key, in Settings › Company › API keys.» Some tools stay a person's even for a
writing key; see MCP server. Do not retry a refusal unchanged.